Compare commits

...

33 Commits

Author SHA1 Message Date
argenis de la rosa b74c5cfda8 fix(gateway): move pairing code below dashboard URL in terminal banner
Repositions the one-time pairing code display to appear directly below
the dashboard URL for cleaner terminal output, and removes the duplicate
display that was showing at the bottom of the route list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 19:50:26 -04:00
Argenis 02688eb124 feat(skills): autonomous skill creation from multi-step tasks (#3916)
Add SkillCreator module that persists successful multi-step task
executions as reusable SKILL.toml definitions under the workspace
skills directory.

- SkillCreationConfig in [skills.skill_creation] (disabled by default)
- Slug validation, TOML generation, embedding-based deduplication
- LRU eviction when max_skills limit is reached
- Agent loop integration post-success
- Gated behind `skill-creation` compile-time feature flag

Closes #3825.
2026-03-18 17:15:02 -04:00
Argenis 2c92cf913b fix: ensure SOUL.md and IDENTITY.md exist in non-tty sessions (#3915)
When the workspace is created outside of `zeroclaw onboard` (e.g., via
cron, daemon, or `< /dev/null`), SOUL.md and IDENTITY.md were never
scaffolded, causing the agent to activate without identity files.

Added `ensure_bootstrap_files()` in `Config::load_or_init()` that
idempotently creates default SOUL.md and IDENTITY.md if missing.

Closes #3819.
2026-03-18 17:12:44 -04:00
Argenis 3c117d2d7b feat(delegate): make sub-agent timeouts configurable via config.toml (#3909)
Add `timeout_secs` and `agentic_timeout_secs` fields to
`DelegateAgentConfig` so users can tune per-agent timeouts instead
of relying on the hardcoded 120s / 300s defaults.

Validation rejects values of 0 or above 3600s, matching the pattern
used by MCP timeout validation.

Closes #3898
2026-03-18 17:07:03 -04:00
Argenis 1f7c3c99e4 feat(i18n): externalize tool descriptions for translation (#3912)
Add a locale-aware tool description system that loads translations from
TOML files in tool_descriptions/. This enables non-English users to see
tool descriptions in their language.

- Add src/i18n.rs module with ToolDescriptions loader, locale detection
  (ZEROCLAW_LOCALE, LANG, LC_ALL env vars), and English fallback chain
- Add locale config field to Config struct for explicit locale override
- Create tool_descriptions/en.toml with all 47 tool descriptions
- Create tool_descriptions/zh-CN.toml with Chinese translations
- Integrate with ToolsSection::build() and build_tool_instructions()
  to resolve descriptions from locale files before hardcoded fallback
- Add PromptContext.tool_descriptions field for prompt-time resolution
- Add AgentBuilder.tool_descriptions() setter for Agent construction
- Include tool_descriptions/ in Cargo.toml package include list
- Add 8 unit tests covering locale loading, fallback chains, env
  detection, and config override

Closes #3901
2026-03-18 17:01:39 -04:00
Argenis 92940a3d16 Merge pull request #3904 from zeroclaw-labs/fix/install-stale-build-cache
fix(install): clean stale build cache on upgrade
2026-03-18 15:49:10 -04:00
Argenis d77c616905 fix: reset tool call dedup cache each iteration to prevent loops (#3910)
The seen_tool_signatures HashSet was initialized outside the iteration loop, causing cross-iteration deduplication of legitimate tool calls. This triggered a self-correction spiral where the agent repeatedly attempted skipped calls until hitting max_iterations.

Moving the HashSet inside the loop ensures deduplication only applies within a single iteration, as originally intended.

Fixes #3798
2026-03-18 15:45:10 -04:00
Argenis ac12470c27 fix(channels): respect ack_reactions config for Telegram channel (#3834) (#3913)
The Telegram channel was ignoring the ack_reactions setting because it
sent setMessageReaction API calls directly in its polling loop, bypassing
the top-level channels_config.ack_reactions check.

- Add optional ack_reactions field to TelegramConfig so it can be set
  under [channels_config.telegram] without "unknown key" warnings
- Add ack_reactions field and with_ack_reactions() builder to
  TelegramChannel, defaulting to true
- Guard try_add_ack_reaction_nonblocking() behind self.ack_reactions
- Wire channel-level override with fallback to top-level default
- Add config deserialization and channel behavior tests
2026-03-18 15:40:31 -04:00
Argenis c5a1148ae9 fix: ensure install.sh creates config.toml and workspace files (#3852) (#3906)
When running install.sh with --docker --skip-build --prefer-prebuilt
(especially with podman via ZEROCLAW_CONTAINER_CLI), the script would
skip creating config.toml and workspace scaffold files because these
were only generated by the onboard wizard, which requires an interactive
terminal or explicit API key.

Add ensure_default_config_and_workspace() that creates a minimal
config.toml (with provider, workspace_dir, and optional api_key/model)
and seeds the workspace directory structure (sessions/, memory/, state/,
cron/, skills/ subdirectories plus IDENTITY.md, USER.md, MEMORY.md,
AGENTS.md, and SOUL.md) when they don't already exist.

This function is called:
- At the end of run_docker_bootstrap(), so config and workspace files
  exist on the host volume regardless of whether onboard ran inside the
  container.
- After the [3/3] Finalizing setup onboard block in the native install
  path, covering --skip-build, --prefer-prebuilt, --skip-onboard, and
  cases where the binary wasn't found.

The function is idempotent: it only writes files that don't already
exist, so it never overwrites config or workspace files created by a
successful onboard run.

Also makes the container onboard failure non-fatal (|| true) so that
the fallback config generation always runs.

Fixes #3852
2026-03-18 15:15:47 -04:00
Argenis 440ad6e5b5 fix: handle double-serialized schedule in cron_add and cron_update (#3860) (#3905)
When LLMs pass the schedule parameter as a JSON string instead of a JSON
object, serde fails with "invalid type: string, expected internally
tagged enum Schedule". Add a deserialize_maybe_stringified helper that
detects stringified JSON values and parses the inner string before
deserializing, providing backward compatibility for both object and
string representations.

Fixes #3860
2026-03-18 15:15:22 -04:00
Argenis 2e41cb56f6 fix: enable vision support for llamacpp provider (#3907)
The llamacpp provider was instantiated with vision disabled by default, causing image transfers from Telegram to fail. Use new_with_vision() with vision enabled, matching the behavior of other compatible providers.

Fixes #3802
2026-03-18 15:14:57 -04:00
Argenis 2227fadb66 fix(tools): include tool_search instruction in deferred tools system prompt (#3826) (#3914)
The deferred MCP tools section in the system prompt only listed tool
names inside <available-deferred-tools> tags without any instruction
telling the LLM to call tool_search to activate them. In daemon and
Telegram mode, where conversations are shorter and less guided, the
LLM never discovered it should call tool_search, so deferred tools
were effectively unavailable.

Add a "## Deferred Tools" heading with explicit instructions that
the LLM MUST call tool_search before using any listed tool. This
ensures the LLM knows to activate deferred tools in all modes
(CLI, daemon, Telegram) consistently.

Also add tests covering:
- Instruction presence in the deferred section
- Multiple-server deferred tool search
- Cross-server keyword search ranking
- Activation persistence across multiple tool_search calls
- Idempotent re-activation
2026-03-18 15:13:58 -04:00
Argenis 162efbb49c fix(providers): recover from context window errors by truncating history (#3908)
When a provider returns a context-size-exceeded error, truncate the
oldest non-system messages from conversation history and retry instead
of immediately bailing out. This enables local models with small
context windows (llamafile, llama.cpp) to work by automatically
fitting the conversation within available context.

Closes #3894
2026-03-18 14:54:56 -04:00
argenis de la rosa 3c8b6d219a fix(test): use PID-scoped script path to prevent ETXTBSY in CI
The echo_provider() test helper writes a fake_claude.sh script to
a shared temp directory. When lib and bin test binaries run in
parallel (separate processes, separate OnceLock statics), one
process can overwrite the script while the other is executing it,
causing "Text file busy" (ETXTBSY). Scope the filename with PID
to isolate each test process.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 14:33:04 -04:00
Vasanth 58b98c59a8 feat(agent): add runtime model switching via model_switch tool (#3853)
Add support for switching AI models at runtime during a conversation.
The model_switch tool allows users to:
- Get current model state
- List available providers
- List models for a provider
- Switch to a different model

The switch takes effect immediately for the current conversation by
recreating the provider with the new model after tool execution.

Risk: Medium - internal state changes and provider recreation
2026-03-18 14:17:52 -04:00
argenis de la rosa d72e9379f7 fix(install): clean stale build cache on upgrade
When upgrading an existing installation, stale build artifacts in
target/release/build/ can cause compilation failures (e.g.
libsqlite3-sys bindgen.rs not found). Run cargo clean --release
before building when an upgrade is detected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 14:15:59 -04:00
Argenis 959b933841 fix(providers): preserve conversation context in Claude Code CLI (#3885)
* fix(providers): preserve conversation context in Claude Code CLI provider

Override chat_with_history to format full multi-turn conversation
history into a single prompt for the claude CLI, instead of only
forwarding the last user message.

Closes #3878

* fix(providers): fix ETXTBSY race in claude_code tests

Use OnceLock to initialize the fake_claude.sh test script exactly
once, preventing "Text file busy" errors when parallel tests
concurrently write and execute the same script file.
2026-03-18 11:13:42 -04:00
Argenis caf7c7194f fix(cron): prevent one-shot jobs from re-executing indefinitely (#3886)
Handle Schedule::At jobs in reschedule_after_run by disabling them
instead of rescheduling to a past timestamp. Also add a fallback in
persist_job_result to disable one-shot jobs if removal fails.

Closes #3868
2026-03-18 11:03:44 -04:00
Argenis ee7d542da6 fix: pass route-specific api_key through channel provider creation (#3881)
When using Channel mode with dynamic classification and routing, the
route-specific `api_key` from `[[model_routes]]` was silently dropped.
The system always fell back to the global `api_key`, causing 401 errors
when routing to `custom:` providers that require distinct credentials.

Root cause: `ChannelRouteSelection` only stored provider + model, and
`get_or_create_provider` always used `ctx.api_key` (the global key).

Changes:
- Add `api_key` field to `ChannelRouteSelection` so the matched route's
  credential survives through to provider creation.
- Update `get_or_create_provider` to accept and prefer a route-specific
  `api_key` over the global key.
- Use a composite cache key (provider name + api_key hash) to prevent
  cache poisoning when multiple routes target the same provider with
  different credentials.
- Wire the route api_key through query classification matching and the
  `/model` (SetModel) command path.

Fixes #3838
2026-03-18 10:06:06 -04:00
Argenis d51ec4b43f fix(docker): remove COPY commands for dockerignored paths (#3880)
The Dockerfile and Dockerfile.debian COPY `firmware/`, `crates/robot-kit/`,
and `crates/robot-kit/Cargo.toml`, but `.dockerignore` excludes both
`firmware/` and `crates/robot-kit/`, causing COPY failures during build.

Since these are hardware-only paths not needed for the Docker runtime:
- Remove COPY commands for `firmware/` and `crates/robot-kit/`
- Remove dummy `crates/robot-kit/src` creation in dep-caching steps
- Use sed to strip `crates/robot-kit` from workspace members in the
  copied Cargo.toml so Cargo doesn't look for the missing manifest

Fixes #3836
2026-03-18 10:06:03 -04:00
Argenis 3d92b2a652 Merge pull request #3833 from zeroclaw-labs/fix/pairing-code-display
fix(web): display pairing code in dashboard
2026-03-17 22:16:50 -04:00
argenis de la rosa 3255051426 fix(web): display pairing code in dashboard instead of terminal-only
Fetch the current pairing code from GET /admin/paircode (localhost-only)
and display it in both the initial PairingDialog and the /pairing
management page. Users no longer need to check the terminal to find
the 6-digit code — it appears directly in the web UI.

Falls back gracefully when the admin endpoint is unreachable (e.g.
non-localhost access), showing the original "check your terminal" prompt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 22:01:03 -04:00
Argenis dcaf330848 Merge pull request #3828 from zeroclaw-labs/fix/readme
fix(readme): update social links across all locales
2026-03-17 19:15:29 -04:00
argenis de la rosa 7f8de5cb17 fix(readme): update Facebook group URL and add Discord, TikTok, RedNote badges
Update Facebook group link from /groups/zeroclaw to /groups/zeroclawlabs
across all 31 README locale files. Add Discord, TikTok, and RedNote
social badges to the badge section of all READMEs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 19:02:53 -04:00
Argenis 1341cfb296 Merge pull request #3827 from zeroclaw-labs/feat/plugin-wasm
feat(plugins): add WASM plugin system with Extism runtime
2026-03-17 18:51:41 -04:00
argenis de la rosa 9da620a5aa fix(ci): add cargo-audit ignore for wasmtime vulns from extism
cargo-audit uses .cargo/audit.toml (not deny.toml) for its ignore
list. These 3 wasmtime advisories are transitive via extism 1.13.0
with no upstream fix available. Plugin system is feature-gated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:38:02 -04:00
argenis de la rosa d016e6b1a0 fix(ci): ignore wasmtime vulns from extism 1.13.0 (no upstream fix)
RUSTSEC-2026-0006, RUSTSEC-2026-0020, RUSTSEC-2026-0021 are all in
wasmtime 37.x pinned by extism. No newer extism release available.
Plugin system is behind a feature flag to limit exposure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:35:08 -04:00
argenis de la rosa 9b6360ad71 fix(ci): ignore unmaintained transitive deps from extism and indicatif
Add cargo-deny ignore entries for RUSTSEC-2024-0388 (derivative),
RUSTSEC-2025-0057 (fxhash), and RUSTSEC-2025-0119 (number_prefix).
All are transitive dependencies we cannot directly control.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:33:03 -04:00
argenis de la rosa dc50ca9171 fix(plugins): update lockfile and fix ws.rs formatting
Sync Cargo.lock with new Extism/WASM plugin dependencies and apply
rustfmt line-wrap fix in gateway WebSocket handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:30:41 -04:00
argenis de la rosa 67edd2bc60 fix(plugins): integrate WASM tools into registry, add gateway routes and tests
- Wire WASM plugin tools into all_tools_with_runtime() behind
  cfg(feature = "plugins-wasm"), discovering and registering tool-capable
  plugins from the configured plugins directory at startup.
- Add /api/plugins gateway endpoint (cfg-gated) for listing plugin status.
- Add mod plugins declaration to main.rs binary crate so crate::plugins
  resolves when the feature is enabled.
- Add unit tests for PluginHost: empty dir, manifest discovery, capability
  filtering, lookup, and removal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:10:24 -04:00
argenis de la rosa dcf66175e4 feat(plugins): add example weather plugin and manifest
Add a standalone example plugin demonstrating the WASM plugin interface:
- example-plugin/Cargo.toml: cdylib crate targeting wasm32-wasip1
- example-plugin/src/lib.rs: mock weather tool using extism-pdk
- example-plugin/manifest.toml: plugin manifest declaring tool capability

This crate is intentionally NOT added to the workspace members since it
targets wasm32-wasip1 and would break the main build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:09:54 -04:00
argenis de la rosa b3bb79d805 feat(plugins): add PluginHost, WasmTool, and WasmChannel bridges
Implement the core plugin infrastructure:
- PluginHost: discovers plugins from the workspace plugins directory,
  loads manifest.toml files, supports install/remove/list/info operations
- WasmTool: bridges WASM plugins to the Tool trait (execute stub pending
  Extism runtime wiring)
- WasmChannel: bridges WASM plugins to the Channel trait (send/listen
  stubs pending Extism runtime wiring)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:09:54 -04:00
argenis de la rosa c857b64bb4 feat(plugins): add Extism dependency, feature flag, and plugin module skeleton
Introduce the WASM plugin system foundation:
- Add extism 1.9 as an optional dependency behind `plugins-wasm` feature
- Create `src/plugins/` module with manifest types, error types, and stub host
- Add `Plugin` CLI subcommands (list, install, remove, info) behind cfg gate
- Add `PluginsConfig` to the config schema with sensible defaults

All plugin code is behind `#[cfg(feature = "plugins-wasm")]` so the default
build is unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:09:54 -04:00
87 changed files with 5844 additions and 287 deletions
+10
View File
@@ -0,0 +1,10 @@
# cargo-audit configuration
# https://rustsec.org/
[advisories]
ignore = [
# wasmtime vulns via extism 1.13.0 — no upstream fix; plugins feature-gated
"RUSTSEC-2026-0006", # wasmtime f64.copysign segfault on x86-64
"RUSTSEC-2026-0020", # WASI guest-controlled resource exhaustion
"RUSTSEC-2026-0021", # WASI http fields panic
]
Generated
+1226 -19
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -31,6 +31,7 @@ include = [
"/LICENSE*",
"/README.md",
"/web/dist/**/*",
"/tool_descriptions/**/*",
]
[dependencies]
@@ -190,6 +191,9 @@ probe-rs = { version = "0.31", optional = true }
# PDF extraction for datasheet RAG (optional, enable with --features rag-pdf)
pdf-extract = { version = "0.10", optional = true }
# WASM plugin runtime (extism)
extism = { version = "1.9", optional = true }
# Terminal QR rendering for WhatsApp Web pairing flow.
qrcode = { version = "0.14", optional = true }
@@ -212,7 +216,7 @@ landlock = { version = "0.4", optional = true }
libc = "0.2"
[features]
default = ["observability-prometheus", "channel-nostr"]
default = ["observability-prometheus", "channel-nostr", "skill-creation"]
channel-nostr = ["dep:nostr-sdk"]
hardware = ["nusb", "tokio-serial"]
channel-matrix = ["dep:matrix-sdk"]
@@ -237,8 +241,12 @@ metrics = ["observability-prometheus"]
probe = ["dep:probe-rs"]
# rag-pdf = PDF ingestion for datasheet RAG
rag-pdf = ["dep:pdf-extract"]
# skill-creation = Autonomous skill creation from successful multi-step tasks
skill-creation = []
# whatsapp-web = Native WhatsApp Web client with custom rusqlite storage backend
whatsapp-web = ["dep:wa-rs", "dep:wa-rs-core", "dep:wa-rs-binary", "dep:wa-rs-proto", "dep:wa-rs-ureq-http", "dep:wa-rs-tokio-transport", "dep:serde-big-array", "dep:prost", "dep:qrcode"]
# WASM plugin system (extism-based)
plugins-wasm = ["dep:extism"]
[profile.release]
opt-level = "z" # Optimize for size
+6 -7
View File
@@ -23,13 +23,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# 1. Copy manifests to cache dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/robot-kit/Cargo.toml crates/robot-kit/Cargo.toml
# Remove robot-kit from workspace members — it is excluded by .dockerignore
# and is not needed for the Docker build (hardware-only crate).
RUN sed -i 's/members = \[".", "crates\/robot-kit"\]/members = ["."]/' Cargo.toml
# Create dummy targets declared in Cargo.toml so manifest parsing succeeds.
RUN mkdir -p src benches crates/robot-kit/src \
RUN mkdir -p src benches \
&& echo "fn main() {}" > src/main.rs \
&& echo "" > src/lib.rs \
&& echo "fn main() {}" > benches/agent_benchmarks.rs \
&& echo "pub fn placeholder() {}" > crates/robot-kit/src/lib.rs
&& echo "fn main() {}" > benches/agent_benchmarks.rs
RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=zeroclaw-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=zeroclaw-target,target=/app/target,sharing=locked \
@@ -38,13 +39,11 @@ RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/regist
else \
cargo build --release --locked; \
fi
RUN rm -rf src benches crates/robot-kit/src
RUN rm -rf src benches
# 2. Copy only build-relevant source paths (avoid cache-busting on docs/tests/scripts)
COPY src/ src/
COPY benches/ benches/
COPY crates/ crates/
COPY firmware/ firmware/
COPY --from=web-builder /web/dist web/dist
COPY *.rs .
RUN touch src/main.rs
+6 -7
View File
@@ -38,13 +38,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# 1. Copy manifests to cache dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/robot-kit/Cargo.toml crates/robot-kit/Cargo.toml
# Remove robot-kit from workspace members — it is excluded by .dockerignore
# and is not needed for the Docker build (hardware-only crate).
RUN sed -i 's/members = \[".", "crates\/robot-kit"\]/members = ["."]/' Cargo.toml
# Create dummy targets declared in Cargo.toml so manifest parsing succeeds.
RUN mkdir -p src benches crates/robot-kit/src \
RUN mkdir -p src benches \
&& echo "fn main() {}" > src/main.rs \
&& echo "" > src/lib.rs \
&& echo "fn main() {}" > benches/agent_benchmarks.rs \
&& echo "pub fn placeholder() {}" > crates/robot-kit/src/lib.rs
&& echo "fn main() {}" > benches/agent_benchmarks.rs
RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=zeroclaw-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=zeroclaw-target,target=/app/target,sharing=locked \
@@ -53,13 +54,11 @@ RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/regist
else \
cargo build --release --locked; \
fi
RUN rm -rf src benches crates/robot-kit/src
RUN rm -rf src benches
# 2. Copy only build-relevant source paths (avoid cache-busting on docs/tests/scripts)
COPY src/ src/
COPY benches/ benches/
COPY crates/ crates/
COPY firmware/ firmware/
COPY --from=web-builder /web/dist web/dist
RUN touch src/main.rs
RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
+5 -2
View File
@@ -16,7 +16,10 @@
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center" dir="rtl">
@@ -103,7 +106,7 @@
| التاريخ (UTC) | المستوى | الإشعار | الإجراء |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _حرج_ | **نحن غير مرتبطين** بـ `openagen/zeroclaw` أو `zeroclaw.org`. نطاق `zeroclaw.org` يشير حاليًا إلى الفرع `openagen/zeroclaw`، وهذا النطاق/المستودع ينتحل شخصية موقعنا/مشروعنا الرسمي. | لا تثق بالمعلومات أو الملفات الثنائية أو جمع التبرعات أو الإعلانات من هذه المصادر. استخدم فقط [هذا المستودع](https://github.com/zeroclaw-labs/zeroclaw) وحساباتنا الموثقة على وسائل التواصل الاجتماعي. |
| 2026-02-21 | _مهم_ | موقعنا الرسمي أصبح متاحًا الآن: [zeroclawlabs.ai](https://zeroclawlabs.ai). شكرًا لصبرك أثناء الانتظار. لا نزال نكتشف محاولات الانتحال: لا تشارك في أي نشاط استثمار/تمويل باسم ZeroClaw إذا لم يتم نشره عبر قنواتنا الرسمية. | استخدم [هذا المستودع](https://github.com/zeroclaw-labs/zeroclaw) كمصدر وحيد للحقيقة. تابع [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21)، [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs)، [Facebook (مجموعة)](https://www.facebook.com/groups/zeroclaw)، [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/)، و[Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) للتحديثات الرسمية. |
| 2026-02-21 | _مهم_ | موقعنا الرسمي أصبح متاحًا الآن: [zeroclawlabs.ai](https://zeroclawlabs.ai). شكرًا لصبرك أثناء الانتظار. لا نزال نكتشف محاولات الانتحال: لا تشارك في أي نشاط استثمار/تمويل باسم ZeroClaw إذا لم يتم نشره عبر قنواتنا الرسمية. | استخدم [هذا المستودع](https://github.com/zeroclaw-labs/zeroclaw) كمصدر وحيد للحقيقة. تابع [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21)، [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs)، [Facebook (مجموعة)](https://www.facebook.com/groups/zeroclawlabs)، [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/)، و[Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) للتحديثات الرسمية. |
| 2026-02-19 | _مهم_ | قامت Anthropic بتحديث شروط استخدام المصادقة وبيانات الاعتماد في 2026-02-19. مصادقة OAuth (Free، Pro، Max) حصريًا لـ Claude Code و Claude.ai؛ استخدام رموز Claude Free/Pro/Max OAuth في أي منتج أو أداة أو خدمة أخرى (بما في ذلك Agent SDK) غير مسموح به وقد ينتهك شروط استخدام المستهلك. | يرجى تجنب مؤقتًا تكاملات Claude Code OAuth لمنع أي خسارة محتملة. البند الأصلي: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ الميزات
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ channels:
## কমিউনিটি
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Použijte tuto tabulku pro důležitá oznámení (změny kompatibility, bezpeč
| Datum (UTC) | Úroveň | Oznámení | Akce |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Kritické_ | **Nejsme propojeni** s `openagen/zeroclaw` nebo `zeroclaw.org`. Doména `zeroclaw.org` aktuálně směřuje na fork `openagen/zeroclaw`, a tato doména/repoziťář se vydává za náš oficiální web/projekt. | Nevěřte informacím, binárním souborům, fundraisingu nebo oznámením z těchto zdrojů. Používejte pouze [tento repoziťář](https://github.com/zeroclaw-labs/zeroclaw) a naše ověřené sociální účty. |
| 2026-02-21 | _Důležité_ | Náš oficiální web je nyní online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Děkujeme za trpělivost během čekání. Stále detekujeme pokusy o vydávání se: neúčastněte žádné investiční/fundraisingové aktivity ve jménu ZeroClaw pokud není publikována přes naše oficiální kanály. | Používejte [tento repoziťář](https://github.com/zeroclaw-labs/zeroclaw) jako jediný zdroj pravdy. Sledujte [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (skupina)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), a [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) pro oficiální aktualizace. |
| 2026-02-21 | _Důležité_ | Náš oficiální web je nyní online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Děkujeme za trpělivost během čekání. Stále detekujeme pokusy o vydávání se: neúčastněte žádné investiční/fundraisingové aktivity ve jménu ZeroClaw pokud není publikována přes naše oficiální kanály. | Používejte [tento repoziťář](https://github.com/zeroclaw-labs/zeroclaw) jako jediný zdroj pravdy. Sledujte [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (skupina)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), a [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) pro oficiální aktualizace. |
| 2026-02-19 | _Důležité_ | Anthropic aktualizoval podmínky použití autentizace a přihlašovacích údajů dne 2026-02-19. OAuth autentizace (Free, Pro, Max) je výhradně pro Claude Code a Claude.ai; použití Claude Free/Pro/Max OAuth tokenů v jakémkoliv jiném produktu, nástroji nebo službě (včetně Agent SDK) není povoleno a může porušit Podmínky použití spotřebitele. | Prosím dočasně se vyhněte Claude Code OAuth integracím pro předcházení potenciálním ztrátám. Původní klauzule: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Funkce
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ Se [LICENSE-APACHE](LICENSE-APACHE) og [LICENSE-MIT](LICENSE-MIT) for detaljer.
## Fællesskab
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -107,7 +110,7 @@ Verwende diese Tabelle für wichtige Hinweise (Kompatibilitätsänderungen, Sich
| Datum (UTC) | Ebene | Hinweis | Aktion |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Kritisch_ | Wir sind **nicht verbunden** mit `openagen/zeroclaw` oder `zeroclaw.org`. Die Domain `zeroclaw.org` zeigt derzeit auf den Fork `openagen/zeroclaw`, und diese Domain/Repository fälscht unsere offizielle Website/Projekt. | Vertraue keinen Informationen, Binärdateien, Fundraising oder Ankündigungen aus diesen Quellen. Verwende nur [dieses Repository](https://github.com/zeroclaw-labs/zeroclaw) und unsere verifizierten Social-Media-Konten. |
| 2026-02-21 | _Wichtig_ | Unsere offizielle Website ist jetzt online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Danke für deine Geduld während der Wartezeit. Wir erkennen weiterhin Fälschungsversuche: nimm an keiner Investitions-/Finanzierungsaktivität im Namen von ZeroClaw teil, wenn sie nicht über unsere offiziellen Kanäle veröffentlicht wird. | Verwende [dieses Repository](https://github.com/zeroclaw-labs/zeroclaw) als einzige Quelle der Wahrheit. Folge [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (Gruppe)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), und [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) für offizielle Updates. |
| 2026-02-21 | _Wichtig_ | Unsere offizielle Website ist jetzt online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Danke für deine Geduld während der Wartezeit. Wir erkennen weiterhin Fälschungsversuche: nimm an keiner Investitions-/Finanzierungsaktivität im Namen von ZeroClaw teil, wenn sie nicht über unsere offiziellen Kanäle veröffentlicht wird. | Verwende [dieses Repository](https://github.com/zeroclaw-labs/zeroclaw) als einzige Quelle der Wahrheit. Folge [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (Gruppe)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), und [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) für offizielle Updates. |
| 2026-02-19 | _Wichtig_ | Anthropic hat die Nutzungsbedingungen für Authentifizierung und Anmeldedaten am 2026-02-19 aktualisiert. Die OAuth-Authentifizierung (Free, Pro, Max) ist ausschließlich für Claude Code und Claude.ai; die Verwendung von Claude Free/Pro/Max OAuth-Token in einem anderen Produkt, Tool oder Dienst (einschließlich Agent SDK) ist nicht erlaubt und kann gegen die Verbrauchernutzungsbedingungen verstoßen. | Bitte vermeide vorübergehend Claude Code OAuth-Integrationen, um potenzielle Verluste zu verhindern. Originalklausel: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Funktionen
+5 -2
View File
@@ -14,7 +14,10 @@
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -176,7 +179,7 @@ channels:
## Κοινότητα
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Usa esta tabla para avisos importantes (cambios de compatibilidad, avisos de seg
| Fecha (UTC) | Nivel | Aviso | Acción |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Crítico_ | **No estamos afiliados** con `openagen/zeroclaw` o `zeroclaw.org`. El dominio `zeroclaw.org` apunta actualmente al fork `openagen/zeroclaw`, y este dominio/repositorio está suplantando nuestro sitio web/proyecto oficial. | No confíes en información, binarios, recaudaciones de fondos o anuncios de estas fuentes. Usa solo [este repositorio](https://github.com/zeroclaw-labs/zeroclaw) y nuestras cuentas sociales verificadas. |
| 2026-02-21 | _Importante_ | Nuestro sitio web oficial ahora está en línea: [zeroclawlabs.ai](https://zeroclawlabs.ai). Gracias por tu paciencia durante la espera. Todavía detectamos intentos de suplantación: no participes en ninguna actividad de inversión/financiamiento en nombre de ZeroClaw si no se publica a través de nuestros canales oficiales. | Usa [este repositorio](https://github.com/zeroclaw-labs/zeroclaw) como la única fuente de verdad. Sigue [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupo)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), y [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) para actualizaciones oficiales. |
| 2026-02-21 | _Importante_ | Nuestro sitio web oficial ahora está en línea: [zeroclawlabs.ai](https://zeroclawlabs.ai). Gracias por tu paciencia durante la espera. Todavía detectamos intentos de suplantación: no participes en ninguna actividad de inversión/financiamiento en nombre de ZeroClaw si no se publica a través de nuestros canales oficiales. | Usa [este repositorio](https://github.com/zeroclaw-labs/zeroclaw) como la única fuente de verdad. Sigue [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupo)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), y [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) para actualizaciones oficiales. |
| 2026-02-19 | _Importante_ | Anthropic actualizó los términos de uso de autenticación y credenciales el 2026-02-19. La autenticación OAuth (Free, Pro, Max) es exclusivamente para Claude Code y Claude.ai; el uso de tokens OAuth de Claude Free/Pro/Max en cualquier otro producto, herramienta o servicio (incluyendo Agent SDK) no está permitido y puede violar los Términos de Uso del Consumidor. | Por favor, evita temporalmente las integraciones OAuth de Claude Code para prevenir cualquier pérdida potencial. Cláusula original: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Características
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ Katso [LICENSE-APACHE](LICENSE-APACHE) ja [LICENSE-MIT](LICENSE-MIT) yksityiskoh
## Yhteisö
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -14,7 +14,10 @@
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributeurs" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Offrez-moi un café" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X : @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit : r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -101,7 +104,7 @@ Utilisez ce tableau pour les avis importants (changements incompatibles, avis de
| Date (UTC) | Niveau | Avis | Action |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Critique_ | Nous ne sommes **pas affiliés** à `openagen/zeroclaw` ou `zeroclaw.org`. Le domaine `zeroclaw.org` pointe actuellement vers le fork `openagen/zeroclaw`, et ce domaine/dépôt usurpe l'identité de notre site web/projet officiel. | Ne faites pas confiance aux informations, binaires, levées de fonds ou annonces provenant de ces sources. Utilisez uniquement [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) et nos comptes sociaux vérifiés. |
| 2026-02-21 | _Important_ | Notre site officiel est désormais en ligne : [zeroclawlabs.ai](https://zeroclawlabs.ai). Merci pour votre patience pendant cette attente. Nous constatons toujours des tentatives d'usurpation : ne participez à aucune activité d'investissement/financement au nom de ZeroClaw si elle n'est pas publiée via nos canaux officiels. | Utilisez [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) comme source unique de vérité. Suivez [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (groupe)](https://www.facebook.com/groups/zeroclaw), et [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) pour les mises à jour officielles. |
| 2026-02-21 | _Important_ | Notre site officiel est désormais en ligne : [zeroclawlabs.ai](https://zeroclawlabs.ai). Merci pour votre patience pendant cette attente. Nous constatons toujours des tentatives d'usurpation : ne participez à aucune activité d'investissement/financement au nom de ZeroClaw si elle n'est pas publiée via nos canaux officiels. | Utilisez [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) comme source unique de vérité. Suivez [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (groupe)](https://www.facebook.com/groups/zeroclawlabs), et [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) pour les mises à jour officielles. |
| 2026-02-19 | _Important_ | Anthropic a mis à jour les conditions d'utilisation de l'authentification et des identifiants le 2026-02-19. L'authentification OAuth (Free, Pro, Max) est exclusivement destinée à Claude Code et Claude.ai ; l'utilisation de tokens OAuth de Claude Free/Pro/Max dans tout autre produit, outil ou service (y compris Agent SDK) n'est pas autorisée et peut violer les Conditions d'utilisation grand public. | Veuillez temporairement éviter les intégrations OAuth de Claude Code pour prévenir toute perte potentielle. Clause originale : [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Fonctionnalités
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center" dir="rtl">
@@ -193,7 +196,7 @@ channels:
## קהילה
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ channels:
## समुदाय
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ Részletekért lásd a [LICENSE-APACHE](LICENSE-APACHE) és [LICENSE-MIT](LICENS
## Közösség
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ Lihat [LICENSE-APACHE](LICENSE-APACHE) dan [LICENSE-MIT](LICENSE-MIT) untuk deta
## Komunitas
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Usa questa tabella per avvisi importanti (cambiamenti di compatibilità, avvisi
| Data (UTC) | Livello | Avviso | Azione |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Critico_ | **Non siamo affiliati** con `openagen/zeroclaw` o `zeroclaw.org`. Il dominio `zeroclaw.org` punta attualmente al fork `openagen/zeroclaw`, e questo dominio/repository sta contraffacendo il nostro sito web/progetto ufficiale. | Non fidarti di informazioni, binari, raccolte fondi o annunci da queste fonti. Usa solo [questo repository](https://github.com/zeroclaw-labs/zeroclaw) e i nostri account social verificati. |
| 2026-02-21 | _Importante_ | Il nostro sito ufficiale è ora online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Grazie per la pazienza durante l'attesa. Rileviamo ancora tentativi di contraffazione: non partecipare ad alcuna attività di investimento/finanziamento a nome di ZeroClaw se non pubblicata tramite i nostri canali ufficiali. | Usa [questo repository](https://github.com/zeroclaw-labs/zeroclaw) come unica fonte di verità. Segui [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (gruppo)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), e [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) per aggiornamenti ufficiali. |
| 2026-02-21 | _Importante_ | Il nostro sito ufficiale è ora online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Grazie per la pazienza durante l'attesa. Rileviamo ancora tentativi di contraffazione: non partecipare ad alcuna attività di investimento/finanziamento a nome di ZeroClaw se non pubblicata tramite i nostri canali ufficiali. | Usa [questo repository](https://github.com/zeroclaw-labs/zeroclaw) come unica fonte di verità. Segui [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (gruppo)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), e [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) per aggiornamenti ufficiali. |
| 2026-02-19 | _Importante_ | Anthropic ha aggiornato i termini di utilizzo di autenticazione e credenziali il 2026-02-19. L'autenticazione OAuth (Free, Pro, Max) è esclusivamente per Claude Code e Claude.ai; l'uso di token OAuth di Claude Free/Pro/Max in qualsiasi altro prodotto, strumento o servizio (incluso Agent SDK) non è consentito e può violare i Termini di Utilizzo del Consumatore. | Si prega di evitare temporaneamente le integrazioni OAuth di Claude Code per prevenire qualsiasi potenziale perdita. Clausola originale: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Funzionalità
+5 -2
View File
@@ -13,7 +13,10 @@
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
@@ -92,7 +95,7 @@
| 日付 (UTC) | レベル | お知らせ | 対応 |
|---|---|---|---|
| 2026-02-19 | _緊急_ | 私たちは `openagen/zeroclaw` および `zeroclaw.org` とは**一切関係ありません**。`zeroclaw.org` は現在 `openagen/zeroclaw` の fork を指しており、そのドメイン/リポジトリは当プロジェクトの公式サイト・公式プロジェクトを装っています。 | これらの情報源による案内、バイナリ、資金調達情報、公式発表は信頼しないでください。必ず[本リポジトリ](https://github.com/zeroclaw-labs/zeroclaw)と認証済み公式SNSのみを参照してください。 |
| 2026-02-21 | _重要_ | 公式サイトを公開しました: [zeroclawlabs.ai](https://zeroclawlabs.ai)。公開までお待ちいただきありがとうございました。引き続きなりすましの試みを確認しているため、ZeroClaw 名義の投資・資金調達などの案内は、公式チャネルで確認できない限り参加しないでください。 | 情報は[本リポジトリ](https://github.com/zeroclaw-labs/zeroclaw)を最優先で確認し、[X@zeroclawlabs](https://x.com/zeroclawlabs?s=21)、[Telegram@zeroclawlabs](https://t.me/zeroclawlabs)、[Facebook(グループ)](https://www.facebook.com/groups/zeroclaw)、[Redditr/zeroclawlabs](https://www.reddit.com/r/zeroclawlabs/) と [小紅書アカウント](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) で公式更新を確認してください。 |
| 2026-02-21 | _重要_ | 公式サイトを公開しました: [zeroclawlabs.ai](https://zeroclawlabs.ai)。公開までお待ちいただきありがとうございました。引き続きなりすましの試みを確認しているため、ZeroClaw 名義の投資・資金調達などの案内は、公式チャネルで確認できない限り参加しないでください。 | 情報は[本リポジトリ](https://github.com/zeroclaw-labs/zeroclaw)を最優先で確認し、[X@zeroclawlabs](https://x.com/zeroclawlabs?s=21)、[Telegram@zeroclawlabs](https://t.me/zeroclawlabs)、[Facebook(グループ)](https://www.facebook.com/groups/zeroclawlabs)、[Redditr/zeroclawlabs](https://www.reddit.com/r/zeroclawlabs/) と [小紅書アカウント](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) で公式更新を確認してください。 |
| 2026-02-19 | _重要_ | Anthropic は 2026-02-19 に Authentication and Credential Use を更新しました。条文では、OAuth authenticationFree/Pro/Max)は Claude Code と Claude.ai 専用であり、Claude Free/Pro/Max で取得した OAuth トークンを他の製品・ツール・サービス(Agent SDK を含む)で使用することは許可されず、Consumer Terms of Service 違反に該当すると明記されています。 | 損失回避のため、当面は Claude Code OAuth 連携を試さないでください。原文: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use)。 |
## 概要
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Harvard, MIT, 그리고 Sundai.Club 커뮤니티의 학생들과 멤버들이
| 날짜 (UTC) | 수준 | 공지 | 조치 |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _중요_ | 우리는 `openagen/zeroclaw` 또는 `zeroclaw.org`**관련이 없습니다**. `zeroclaw.org` 도메인은 현재 `openagen/zeroclaw` 포크를 가리키고 있으며, 이 도메인/저장소는 우리의 공식 웹사이트/프로젝트를 사칭하고 있습니다. | 이 소스의 정보, 바이너리, 펀딩, 공지를 신뢰하지 마세요. [이 저장소](https://github.com/zeroclaw-labs/zeroclaw)와 우리의 확인된 소셜 계정만 사용하세요. |
| 2026-02-21 | _중요_ | 우리의 공식 웹사이트가 이제 온라인입니다: [zeroclawlabs.ai](https://zeroclawlabs.ai). 기다려주셔서 감사합니다. 여전히 사칭 시도가 감지되고 있습니다: 공식 채널을 통해 게시되지 않은 ZeroClaw 이름의 모든 투자/펀딩 활동에 참여하지 마세요. | [이 저장소](https://github.com/zeroclaw-labs/zeroclaw)를 유일한 진실의 원천으로 사용하세요. [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (그룹)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), 그리고 [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search)를 팔로우하여 공식 업데이트를 받으세요. |
| 2026-02-21 | _중요_ | 우리의 공식 웹사이트가 이제 온라인입니다: [zeroclawlabs.ai](https://zeroclawlabs.ai). 기다려주셔서 감사합니다. 여전히 사칭 시도가 감지되고 있습니다: 공식 채널을 통해 게시되지 않은 ZeroClaw 이름의 모든 투자/펀딩 활동에 참여하지 마세요. | [이 저장소](https://github.com/zeroclaw-labs/zeroclaw)를 유일한 진실의 원천으로 사용하세요. [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (그룹)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), 그리고 [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search)를 팔로우하여 공식 업데이트를 받으세요. |
| 2026-02-19 | _중요_ | Anthropic이 2026-02-19에 인증 및 자격증명 사용 약관을 업데이트했습니다. OAuth 인증(Free, Pro, Max)은 Claude Code 및 Claude.ai 전용입니다. 다른 제품, 도구 또는 서비스(Agent SDK 포함)에서 Claude Free/Pro/Max OAuth 토큰을 사용하는 것은 허용되지 않으며 소비자 이용약관을 위반할 수 있습니다. | 잠재적인 손실을 방지하기 위해 일시적으로 Claude Code OAuth 통합을 피하세요. 원본 조항: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ 기능
+5 -2
View File
@@ -14,7 +14,10 @@
<a href="https://github.com/zeroclaw-labs/zeroclaw/graphs/contributors"><img src="https://img.shields.io/github/contributors/zeroclaw-labs/zeroclaw?color=green" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -94,7 +97,7 @@ Use this board for important notices (breaking changes, security advisories, mai
| Date (UTC) | Level | Notice | Action |
| ---------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Critical_ | We are **not affiliated** with `openagen/zeroclaw`, `zeroclaw.org` or `zeroclaw.net`. The `zeroclaw.org` and `zeroclaw.net` domains currently points to the `openagen/zeroclaw` fork, and that domain/repository are impersonating our official website/project. | Do not trust information, binaries, fundraising, or announcements from those sources. Use only [this repository](https://github.com/zeroclaw-labs/zeroclaw) and our verified social accounts. |
| 2026-02-21 | _Important_ | Our official website is now live: [zeroclawlabs.ai](https://zeroclawlabs.ai). Thanks for your patience while we prepared the launch. We are still seeing impersonation attempts, so do **not** join any investment or fundraising activity claiming the ZeroClaw name unless it is published through our official channels. | Use [this repository](https://github.com/zeroclaw-labs/zeroclaw) as the single source of truth. Follow [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (Group)](https://www.facebook.com/groups/zeroclaw), and [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) for official updates. |
| 2026-02-21 | _Important_ | Our official website is now live: [zeroclawlabs.ai](https://zeroclawlabs.ai). Thanks for your patience while we prepared the launch. We are still seeing impersonation attempts, so do **not** join any investment or fundraising activity claiming the ZeroClaw name unless it is published through our official channels. | Use [this repository](https://github.com/zeroclaw-labs/zeroclaw) as the single source of truth. Follow [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (Group)](https://www.facebook.com/groups/zeroclawlabs), and [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) for official updates. |
| 2026-02-19 | _Important_ | Anthropic updated the Authentication and Credential Use terms on 2026-02-19. Claude Code OAuth tokens (Free, Pro, Max) are intended exclusively for Claude Code and Claude.ai; using OAuth tokens from Claude Free/Pro/Max in any other product, tool, or service (including Agent SDK) is not permitted and may violate the Consumer Terms of Service. | Please temporarily avoid Claude Code OAuth integrations to prevent potential loss. Original clause: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Features
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ Se [LICENSE-APACHE](LICENSE-APACHE) og [LICENSE-MIT](LICENSE-MIT) for detaljer.
## Fellesskap
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Gebruik deze tabel voor belangrijke aankondigingen (compatibiliteitswijzigingen,
| Datum (UTC) | Niveau | Aankondiging | Actie |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Kritiek_ | **We zijn niet gelieerd** met `openagen/zeroclaw` of `zeroclaw.org`. Het domein `zeroclaw.org` wijst momenteel naar de fork `openagen/zeroclaw`, en dit domein/repository imiteert onze officiële website/project. | Vertrouw geen informatie, binaire bestanden, fondsenwerving of aankondigingen van deze bronnen. Gebruik alleen [deze repository](https://github.com/zeroclaw-labs/zeroclaw) en onze geverifieerde sociale media accounts. |
| 2026-02-21 | _Belangrijk_ | Onze officiële website is nu online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Bedankt voor je geduld tijdens het wachten. We detecteren nog steeds imitatiepogingen: neem niet deel aan enige investering/fondsenwerving activiteit in naam van ZeroClaw als deze niet via onze officiële kanalen wordt gepubliceerd. | Gebruik [deze repository](https://github.com/zeroclaw-labs/zeroclaw) als de enige bron van waarheid. Volg [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (groep)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), en [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) voor officiële updates. |
| 2026-02-21 | _Belangrijk_ | Onze officiële website is nu online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Bedankt voor je geduld tijdens het wachten. We detecteren nog steeds imitatiepogingen: neem niet deel aan enige investering/fondsenwerving activiteit in naam van ZeroClaw als deze niet via onze officiële kanalen wordt gepubliceerd. | Gebruik [deze repository](https://github.com/zeroclaw-labs/zeroclaw) als de enige bron van waarheid. Volg [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (groep)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), en [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) voor officiële updates. |
| 2026-02-19 | _Belangrijk_ | Anthropic heeft de gebruiksvoorwaarden voor authenticatie en inloggegevens bijgewerkt op 2026-02-19. OAuth authenticatie (Free, Pro, Max) is exclusief voor Claude Code en Claude.ai; het gebruik van Claude Free/Pro/Max OAuth tokens in enig ander product, tool of service (inclusief Agent SDK) is niet toegestaan en kan in strijd zijn met de Consumenten Gebruiksvoorwaarden. | Vermijd tijdelijk Claude Code OAuth integraties om potentiële verliezen te voorkomen. Originele clausule: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Functies
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Użyj tej tabeli dla ważnych ogłoszeń (zmiany kompatybilności, powiadomienia
| Data (UTC) | Poziom | Ogłoszenie | Działanie |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Krytyczny_ | **Nie jesteśmy powiązani** z `openagen/zeroclaw` lub `zeroclaw.org`. Domena `zeroclaw.org` obecnie wskazuje na fork `openagen/zeroclaw`, i ta domena/repozytorium podszywa się pod naszą oficjalną stronę/projekt. | Nie ufaj informacjom, plikom binarnym, zbiórkom funduszy lub ogłoszeniom z tych źródeł. Używaj tylko [tego repozytorium](https://github.com/zeroclaw-labs/zeroclaw) i naszych zweryfikowanych kont społecznościowych. |
| 2026-02-21 | _Ważne_ | Nasza oficjalna strona jest teraz online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Dziękujemy za cierpliwość podczas oczekiwania. Nadal wykrywamy próby podszywania się: nie uczestnicz w żadnej działalności inwestycyjnej/finansowej w imieniu ZeroClaw jeśli nie jest opublikowana przez nasze oficjalne kanały. | Używaj [tego repozytorium](https://github.com/zeroclaw-labs/zeroclaw) jako jedynego źródła prawdy. Śledź [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupa)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), i [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) dla oficjalnych aktualizacji. |
| 2026-02-21 | _Ważne_ | Nasza oficjalna strona jest teraz online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Dziękujemy za cierpliwość podczas oczekiwania. Nadal wykrywamy próby podszywania się: nie uczestnicz w żadnej działalności inwestycyjnej/finansowej w imieniu ZeroClaw jeśli nie jest opublikowana przez nasze oficjalne kanały. | Używaj [tego repozytorium](https://github.com/zeroclaw-labs/zeroclaw) jako jedynego źródła prawdy. Śledź [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupa)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), i [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) dla oficjalnych aktualizacji. |
| 2026-02-19 | _Ważne_ | Anthropic zaktualizował warunki używania uwierzytelniania i poświadczeń 2026-02-19. Uwierzytelnianie OAuth (Free, Pro, Max) jest wyłącznie dla Claude Code i Claude.ai; używanie tokenów OAuth Claude Free/Pro/Max w jakimkolwiek innym produkcie, narzędziu lub usłudze (w tym Agent SDK) nie jest dozwolone i może naruszać Warunki Użytkowania Konsumenta. | Prosimy tymczasowo unikać integracji OAuth Claude Code aby zapobiec potencjalnym stratom. Oryginalna klauzula: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Funkcje
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Use esta tabela para avisos importantes (mudanças de compatibilidade, avisos de
| Data (UTC) | Nível | Aviso | Ação |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Crítico_ | **Não somos afiliados** ao `openagen/zeroclaw` ou `zeroclaw.org`. O domínio `zeroclaw.org` atualmente aponta para o fork `openagen/zeroclaw`, e este domínio/repositório está falsificando nosso site/projeto oficial. | Não confie em informações, binários, arrecadações ou anúncios dessas fontes. Use apenas [este repositório](https://github.com/zeroclaw-labs/zeroclaw) e nossas contas sociais verificadas. |
| 2026-02-21 | _Importante_ | Nosso site oficial agora está online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Obrigado pela paciência durante a espera. Ainda detectamos tentativas de falsificação: não participe de nenhuma atividade de investimento/financiamento em nome do ZeroClaw se não for publicada através de nossos canais oficiais. | Use [este repositório](https://github.com/zeroclaw-labs/zeroclaw) como a única fonte de verdade. Siga [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupo)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), e [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) para atualizações oficiais. |
| 2026-02-21 | _Importante_ | Nosso site oficial agora está online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Obrigado pela paciência durante a espera. Ainda detectamos tentativas de falsificação: não participe de nenhuma atividade de investimento/financiamento em nome do ZeroClaw se não for publicada através de nossos canais oficiais. | Use [este repositório](https://github.com/zeroclaw-labs/zeroclaw) como a única fonte de verdade. Siga [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupo)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), e [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) para atualizações oficiais. |
| 2026-02-19 | _Importante_ | A Anthropic atualizou os termos de uso de autenticação e credenciais em 2026-02-19. A autenticação OAuth (Free, Pro, Max) é exclusivamente para Claude Code e Claude.ai; o uso de tokens OAuth do Claude Free/Pro/Max em qualquer outro produto, ferramenta ou serviço (incluindo Agent SDK) não é permitido e pode violar os Termos de Uso do Consumidor. | Por favor, evite temporariamente as integrações OAuth do Claude Code para prevenir qualquer perda potencial. Cláusula original: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Funcionalidades
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ Vezi [LICENSE-APACHE](LICENSE-APACHE) și [LICENSE-MIT](LICENSE-MIT) pentru deta
## Comunitate
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -13,7 +13,10 @@
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
@@ -92,7 +95,7 @@
| Дата (UTC) | Уровень | Объявление | Действие |
|---|---|---|---|
| 2026-02-19 | _Срочно_ | Мы **не аффилированы** с `openagen/zeroclaw` и `zeroclaw.org`. Домен `zeroclaw.org` сейчас указывает на fork `openagen/zeroclaw`, и этот домен/репозиторий выдают себя за наш официальный сайт и проект. | Не доверяйте информации, бинарникам, сборам средств и «официальным» объявлениям из этих источников. Используйте только [этот репозиторий](https://github.com/zeroclaw-labs/zeroclaw) и наши верифицированные соцсети. |
| 2026-02-21 | _Важно_ | Наш официальный сайт уже запущен: [zeroclawlabs.ai](https://zeroclawlabs.ai). Спасибо, что дождались запуска. При этом попытки выдавать себя за ZeroClaw продолжаются, поэтому не участвуйте в инвестициях, сборах средств и похожих активностях, если они не подтверждены через наши официальные каналы. | Ориентируйтесь только на [этот репозиторий](https://github.com/zeroclaw-labs/zeroclaw); также следите за [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (группа)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) и [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) для официальных обновлений. |
| 2026-02-21 | _Важно_ | Наш официальный сайт уже запущен: [zeroclawlabs.ai](https://zeroclawlabs.ai). Спасибо, что дождались запуска. При этом попытки выдавать себя за ZeroClaw продолжаются, поэтому не участвуйте в инвестициях, сборах средств и похожих активностях, если они не подтверждены через наши официальные каналы. | Ориентируйтесь только на [этот репозиторий](https://github.com/zeroclaw-labs/zeroclaw); также следите за [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (группа)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) и [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) для официальных обновлений. |
| 2026-02-19 | _Важно_ | Anthropic обновил раздел Authentication and Credential Use 2026-02-19. В нем указано, что OAuth authentication (Free/Pro/Max) предназначена только для Claude Code и Claude.ai; использование OAuth-токенов, полученных через Claude Free/Pro/Max, в любых других продуктах, инструментах или сервисах (включая Agent SDK), не допускается и может считаться нарушением Consumer Terms of Service. | Чтобы избежать потерь, временно не используйте Claude Code OAuth-интеграции. Оригинал: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
## О проекте
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ Se [LICENSE-APACHE](LICENSE-APACHE) och [LICENSE-MIT](LICENSE-MIT) för detaljer
## Community
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ channels:
## ชุมชน
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Gamitin ang talahanayang ito para sa mahahalagang paunawa (compatibility changes
| Petsa (UTC) | Antas | Paunawa | Aksyon |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Kritikal_ | **Hindi kami kaugnay** sa `openagen/zeroclaw` o `zeroclaw.org`. Ang domain na `zeroclaw.org` ay kasalukuyang tumuturo sa fork na `openagen/zeroclaw`, at ang domain/repository na ito ay nanggagaya sa aming opisyal na website/proyekto. | Huwag magtiwala sa impormasyon, binaries, fundraising, o mga anunsyo mula sa mga pinagmulang ito. Gamitin lamang [ang repository na ito](https://github.com/zeroclaw-labs/zeroclaw) at aming mga verified social media accounts. |
| 2026-02-21 | _Mahalaga_ | Ang aming opisyal na website ay ngayon online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Salamat sa iyong pasensya sa panahon ng paghihintay. Nakikita pa rin namin ang mga pagtatangka ng panliliko: huwag lumahok sa anumang investment/funding activity sa ngalan ng ZeroClaw kung hindi ito nai-publish sa pamamagitan ng aming mga opisyal na channel. | Gamitin [ang repository na ito](https://github.com/zeroclaw-labs/zeroclaw) bilang nag-iisang source of truth. Sundan [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupo)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), at [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) para sa mga opisyal na update. |
| 2026-02-21 | _Mahalaga_ | Ang aming opisyal na website ay ngayon online: [zeroclawlabs.ai](https://zeroclawlabs.ai). Salamat sa iyong pasensya sa panahon ng paghihintay. Nakikita pa rin namin ang mga pagtatangka ng panliliko: huwag lumahok sa anumang investment/funding activity sa ngalan ng ZeroClaw kung hindi ito nai-publish sa pamamagitan ng aming mga opisyal na channel. | Gamitin [ang repository na ito](https://github.com/zeroclaw-labs/zeroclaw) bilang nag-iisang source of truth. Sundan [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grupo)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), at [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) para sa mga opisyal na update. |
| 2026-02-19 | _Mahalaga_ | In-update ng Anthropic ang authentication at credential use terms noong 2026-02-19. Ang OAuth authentication (Free, Pro, Max) ay eksklusibo para sa Claude Code at Claude.ai; ang paggamit ng Claude Free/Pro/Max OAuth tokens sa anumang iba pang produkto, tool, o serbisyo (kasama ang Agent SDK) ay hindi pinapayagan at maaaring lumabag sa Consumer Terms of Use. | Mangyaring pansamantalang iwasan ang Claude Code OAuth integrations upang maiwasan ang anumang potensyal na pagkawala. Orihinal na clause: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Mga Tampok
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -103,7 +106,7 @@ Harvard, MIT ve Sundai.Club topluluklarının öğrencileri ve üyeleri tarafın
| Tarih (UTC) | Seviye | Duyuru | Eylem |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Kritik_ | **`openagen/zeroclaw` veya `zeroclaw.org` ile bağlantılı değiliz.** `zeroclaw.org` alanı şu anda `openagen/zeroclaw` fork'una işaret ediyor ve bu alan/depo taklitçiliğini yapıyor. | Bu kaynaklardan bilgi, ikili dosyalar, bağış toplama veya duyurulara güvenmeyin. Sadece [bu depoyu](https://github.com/zeroclaw-labs/zeroclaw) ve doğrulanmış sosyal medya hesaplarımızı kullanın. |
| 2026-02-21 | _Önemli_ | Resmi web sitemiz artık çevrimiçi: [zeroclawlabs.ai](https://zeroclawlabs.ai). Bekleme sürecinde sabırlarınız için teşekkürler. Hala taklit girişimleri tespit ediyoruz: ZeroClaw adına resmi kanallarımız aracılığıyla yayınlanmayan herhangi bir yatırım/bağış faaliyetine katılmayın. | [Bu depoyu](https://github.com/zeroclaw-labs/zeroclaw) tek doğruluk kaynağı olarak kullanın. Resmi güncellemeler için [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grup)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) ve [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search)'u takip edin. |
| 2026-02-21 | _Önemli_ | Resmi web sitemiz artık çevrimiçi: [zeroclawlabs.ai](https://zeroclawlabs.ai). Bekleme sürecinde sabırlarınız için teşekkürler. Hala taklit girişimleri tespit ediyoruz: ZeroClaw adına resmi kanallarımız aracılığıyla yayınlanmayan herhangi bir yatırım/bağış faaliyetine katılmayın. | [Bu depoyu](https://github.com/zeroclaw-labs/zeroclaw) tek doğruluk kaynağı olarak kullanın. Resmi güncellemeler için [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (grup)](https://www.facebook.com/groups/zeroclawlabs), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) ve [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search)'u takip edin. |
| 2026-02-19 | _Önemli_ | Anthropic, 2026-02-19 tarihinde kimlik doğrulama ve kimlik bilgileri kullanım şartlarını güncelledi. OAuth kimlik doğrulaması (Free, Pro, Max) yalnızca Claude Code ve Claude.ai içindir; Claude Free/Pro/Max OAuth belirteçlerini başka herhangi bir ürün, araç veya hizmette (Agent SDK dahil) kullanmak yasaktır ve Tüketici Kullanım Şartlarını ihlal edebilir. | Olası kayıpları önlemek için lütfen geçici olarak Claude Code OAuth entegrasyonlarından kaçının. Orijinal madde: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Özellikler
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center">
@@ -177,7 +180,7 @@ channels:
## Спільнота
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -17,7 +17,10 @@
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
</p>
<p align="center" dir="rtl">
@@ -193,7 +196,7 @@ channels:
## کمیونٹی
- [Telegram](https://t.me/zeroclawlabs)
- [Facebook Group](https://www.facebook.com/groups/zeroclaw)
- [Facebook Group](https://www.facebook.com/groups/zeroclawlabs)
- [WeChat Group](https://zeroclawlabs.cn/group.jpg)
---
+5 -2
View File
@@ -14,7 +14,10 @@
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
@@ -101,7 +104,7 @@ Bảng này dành cho các thông báo quan trọng (thay đổi không tương
| Ngày (UTC) | Mức độ | Thông báo | Hành động |
|---|---|---|---|
| 2026-02-19 | _Nghiêm trọng_ | Chúng tôi **không có liên kết** với `openagen/zeroclaw` hoặc `zeroclaw.org`. Tên miền `zeroclaw.org` hiện đang trỏ đến fork `openagen/zeroclaw`, và tên miền/repository đó đang mạo danh website/dự án chính thức của chúng tôi. | Không tin tưởng thông tin, binary, gây quỹ, hay thông báo từ các nguồn đó. Chỉ sử dụng [repository này](https://github.com/zeroclaw-labs/zeroclaw) và các tài khoản mạng xã hội đã được xác minh của chúng tôi. |
| 2026-02-21 | _Quan trọng_ | Website chính thức của chúng tôi đã ra mắt: [zeroclawlabs.ai](https://zeroclawlabs.ai). Cảm ơn mọi người đã kiên nhẫn chờ đợi. Chúng tôi vẫn đang ghi nhận các nỗ lực mạo danh, vì vậy **không** tham gia bất kỳ hoạt động đầu tư hoặc gây quỹ nào nhân danh ZeroClaw nếu thông tin đó không được công bố qua các kênh chính thức của chúng tôi. | Sử dụng [repository này](https://github.com/zeroclaw-labs/zeroclaw) làm nguồn thông tin duy nhất đáng tin cậy. Theo dõi [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (nhóm)](https://www.facebook.com/groups/zeroclaw), và [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) để nhận cập nhật chính thức. |
| 2026-02-21 | _Quan trọng_ | Website chính thức của chúng tôi đã ra mắt: [zeroclawlabs.ai](https://zeroclawlabs.ai). Cảm ơn mọi người đã kiên nhẫn chờ đợi. Chúng tôi vẫn đang ghi nhận các nỗ lực mạo danh, vì vậy **không** tham gia bất kỳ hoạt động đầu tư hoặc gây quỹ nào nhân danh ZeroClaw nếu thông tin đó không được công bố qua các kênh chính thức của chúng tôi. | Sử dụng [repository này](https://github.com/zeroclaw-labs/zeroclaw) làm nguồn thông tin duy nhất đáng tin cậy. Theo dõi [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Facebook (nhóm)](https://www.facebook.com/groups/zeroclawlabs), và [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) để nhận cập nhật chính thức. |
| 2026-02-19 | _Quan trọng_ | Anthropic đã cập nhật điều khoản Xác thực và Sử dụng Thông tin xác thực vào ngày 2026-02-19. Xác thực OAuth (Free, Pro, Max) được dành riêng cho Claude Code và Claude.ai; việc sử dụng OAuth token từ Claude Free/Pro/Max trong bất kỳ sản phẩm, công cụ hay dịch vụ nào khác (bao gồm Agent SDK) đều không được phép và có thể vi phạm Điều khoản Dịch vụ cho Người tiêu dùng. | Vui lòng tạm thời tránh tích hợp Claude Code OAuth để ngăn ngừa khả năng mất mát. Điều khoản gốc: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Tính năng
+5 -2
View File
@@ -13,7 +13,10 @@
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.facebook.com/groups/zeroclawlabs"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://discord.com/invite/wDshRVqRjx"><img src="https://img.shields.io/badge/Discord-Join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://www.tiktok.com/@zeroclawlabs"><img src="https://img.shields.io/badge/TikTok-%40zeroclawlabs-000000?style=flat&logo=tiktok&logoColor=white" alt="TikTok: @zeroclawlabs" /></a>
<a href="https://www.rednote.com/user/profile/69b735e6000000002603927e"><img src="https://img.shields.io/badge/RedNote-Official-FF2442?style=flat" alt="RedNote" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
@@ -92,7 +95,7 @@
| 日期(UTC) | 级别 | 通知 | 处理建议 |
|---|---|---|---|
| 2026-02-19 | _紧急_ | 我们与 `openagen/zeroclaw``zeroclaw.org` **没有任何关系**`zeroclaw.org` 当前会指向 `openagen/zeroclaw` 这个 fork,并且该域名/仓库正在冒充我们的官网与官方项目。 | 请不要相信上述来源发布的任何信息、二进制、募资活动或官方声明。请仅以[本仓库](https://github.com/zeroclaw-labs/zeroclaw)和已验证官方社媒为准。 |
| 2026-02-21 | _重要_ | 我们的官网现已上线:[zeroclawlabs.ai](https://zeroclawlabs.ai)。感谢大家一直以来的耐心等待。我们仍在持续发现冒充行为,请勿参与任何未经我们官方渠道发布、但打着 ZeroClaw 名义进行的投资、募资或类似活动。 | 一切信息请以[本仓库](https://github.com/zeroclaw-labs/zeroclaw)为准;也可关注 [X@zeroclawlabs](https://x.com/zeroclawlabs?s=21)、[Telegram@zeroclawlabs](https://t.me/zeroclawlabs)、[Facebook(群组)](https://www.facebook.com/groups/zeroclaw)、[Redditr/zeroclawlabs](https://www.reddit.com/r/zeroclawlabs/) 与 [小红书账号](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) 获取官方最新动态。 |
| 2026-02-21 | _重要_ | 我们的官网现已上线:[zeroclawlabs.ai](https://zeroclawlabs.ai)。感谢大家一直以来的耐心等待。我们仍在持续发现冒充行为,请勿参与任何未经我们官方渠道发布、但打着 ZeroClaw 名义进行的投资、募资或类似活动。 | 一切信息请以[本仓库](https://github.com/zeroclaw-labs/zeroclaw)为准;也可关注 [X@zeroclawlabs](https://x.com/zeroclawlabs?s=21)、[Telegram@zeroclawlabs](https://t.me/zeroclawlabs)、[Facebook(群组)](https://www.facebook.com/groups/zeroclawlabs)、[Redditr/zeroclawlabs](https://www.reddit.com/r/zeroclawlabs/) 与 [小红书账号](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) 获取官方最新动态。 |
| 2026-02-19 | _重要_ | Anthropic 于 2026-02-19 更新了 Authentication and Credential Use 条款。条款明确:OAuth authentication(用于 Free、Pro、Max)仅适用于 Claude Code 与 Claude.ai;将 Claude Free/Pro/Max 账号获得的 OAuth token 用于其他任何产品、工具或服务(包括 Agent SDK)不被允许,并可能构成对 Consumer Terms of Service 的违规。 | 为避免损失,请暂时不要尝试 Claude Code OAuth 集成;原文见:[Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use)。 |
## 项目简介
+7
View File
@@ -12,6 +12,13 @@ ignore = [
# bincode v2.0.1 via probe-rs — project ceased but 1.3.3 considered complete
"RUSTSEC-2025-0141",
{ id = "RUSTSEC-2024-0384", reason = "Reported to `rust-nostr/nostr` and it's WIP" },
{ id = "RUSTSEC-2024-0388", reason = "derivative via extism → wasmtime transitive dep" },
{ id = "RUSTSEC-2025-0057", reason = "fxhash via extism → wasmtime transitive dep" },
{ id = "RUSTSEC-2025-0119", reason = "number_prefix via indicatif — cosmetic dep" },
# wasmtime vulns via extism 1.13.0 — no upstream fix yet; plugins feature-gated
{ id = "RUSTSEC-2026-0006", reason = "wasmtime segfault via extism; awaiting extism upgrade" },
{ id = "RUSTSEC-2026-0020", reason = "WASI resource exhaustion via extism; awaiting extism upgrade" },
{ id = "RUSTSEC-2026-0021", reason = "WASI http fields panic via extism; awaiting extism upgrade" },
]
[licenses]
+4
View File
@@ -183,6 +183,8 @@ Delegate sub-agent configurations. Each key under `[agents]` defines a named sub
| `agentic` | `false` | Enable multi-turn tool-call loop mode for the sub-agent |
| `allowed_tools` | `[]` | Tool allowlist for agentic mode |
| `max_iterations` | `10` | Max tool-call iterations for agentic mode |
| `timeout_secs` | `120` | Timeout in seconds for non-agentic provider calls (13600) |
| `agentic_timeout_secs` | `300` | Timeout in seconds for agentic sub-agent loops (13600) |
Notes:
@@ -199,11 +201,13 @@ max_depth = 2
agentic = true
allowed_tools = ["web_search", "http_request", "file_read"]
max_iterations = 8
agentic_timeout_secs = 600
[agents.coder]
provider = "ollama"
model = "qwen2.5-coder:32b"
temperature = 0.2
timeout_secs = 60
```
## `[runtime]`
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "zeroclaw-weather-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
extism-pdk = "1.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+8
View File
@@ -0,0 +1,8 @@
name = "weather"
version = "0.1.0"
description = "Example weather tool plugin for ZeroClaw"
author = "ZeroClaw Labs"
wasm_path = "target/wasm32-wasip1/release/zeroclaw_weather_plugin.wasm"
capabilities = ["tool"]
permissions = ["http_client"]
+42
View File
@@ -0,0 +1,42 @@
//! Example ZeroClaw weather plugin.
//!
//! Demonstrates how to create a WASM tool plugin using extism-pdk.
//! Build with: cargo build --target wasm32-wasip1 --release
use extism_pdk::*;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct WeatherInput {
location: String,
}
#[derive(Serialize)]
struct WeatherOutput {
location: String,
temperature: f64,
unit: String,
condition: String,
humidity: u32,
}
/// Get weather for a location (mock implementation for demonstration).
#[plugin_fn]
pub fn get_weather(input: String) -> FnResult<String> {
let params: WeatherInput =
serde_json::from_str(&input).map_err(|e| Error::msg(format!("invalid input: {e}")))?;
// Mock weather data for demonstration
let output = WeatherOutput {
location: params.location,
temperature: 22.5,
unit: "celsius".to_string(),
condition: "Partly cloudy".to_string(),
humidity: 65,
};
let json = serde_json::to_string(&output)
.map_err(|e| Error::msg(format!("serialization error: {e}")))?;
Ok(json)
}
+155 -1
View File
@@ -767,6 +767,140 @@ run_guided_installer() {
fi
}
ensure_default_config_and_workspace() {
# Creates a minimal config.toml and workspace scaffold files when the
# onboard wizard was skipped (e.g. --skip-build --prefer-prebuilt, or
# Docker mode without an API key).
#
# $1 — config directory (e.g. ~/.zeroclaw or $docker_data_dir/.zeroclaw)
# $2 — workspace directory (e.g. ~/.zeroclaw/workspace or $docker_data_dir/workspace)
# $3 — provider name (default: openrouter)
local config_dir="$1"
local workspace_dir="$2"
local provider="${3:-openrouter}"
mkdir -p "$config_dir" "$workspace_dir"
# --- config.toml ---
local config_path="$config_dir/config.toml"
if [[ ! -f "$config_path" ]]; then
step_dot "Creating default config.toml"
cat > "$config_path" <<TOML
# ZeroClaw configuration — generated by install.sh
# Edit this file or run 'zeroclaw onboard' to reconfigure.
default_provider = "${provider}"
workspace_dir = "${workspace_dir}"
TOML
if [[ -n "${API_KEY:-}" ]]; then
printf 'api_key = "%s"\n' "$API_KEY" >> "$config_path"
fi
if [[ -n "${MODEL:-}" ]]; then
printf 'default_model = "%s"\n' "$MODEL" >> "$config_path"
fi
chmod 600 "$config_path" 2>/dev/null || true
step_ok "Default config.toml created at $config_path"
else
step_dot "config.toml already exists, skipping"
fi
# --- Workspace scaffold ---
local subdirs=(sessions memory state cron skills)
for dir in "${subdirs[@]}"; do
mkdir -p "$workspace_dir/$dir"
done
# Seed workspace markdown files only if they don't already exist.
local user_name="${USER:-User}"
local agent_name="ZeroClaw"
_write_if_missing() {
local filepath="$1"
local content="$2"
if [[ ! -f "$filepath" ]]; then
printf '%s\n' "$content" > "$filepath"
fi
}
_write_if_missing "$workspace_dir/IDENTITY.md" \
"# IDENTITY.md — Who Am I?
- **Name:** ${agent_name}
- **Creature:** A Rust-forged AI — fast, lean, and relentless
- **Vibe:** Sharp, direct, resourceful. Not corporate. Not a chatbot.
---
Update this file as you evolve. Your identity is yours to shape."
_write_if_missing "$workspace_dir/USER.md" \
"# USER.md — Who You're Helping
## About You
- **Name:** ${user_name}
- **Timezone:** UTC
- **Languages:** English
## Preferences
- (Add your preferences here)
## Work Context
- (Add your work context here)
---
*Update this anytime. The more ${agent_name} knows, the better it helps.*"
_write_if_missing "$workspace_dir/MEMORY.md" \
"# MEMORY.md — Long-Term Memory
## Key Facts
(Add important facts here)
## Decisions & Preferences
(Record decisions and preferences here)
## Lessons Learned
(Document mistakes and insights here)
## Open Loops
(Track unfinished tasks and follow-ups here)"
_write_if_missing "$workspace_dir/AGENTS.md" \
"# AGENTS.md — ${agent_name} Personal Assistant
## Every Session (required)
Before doing anything else:
1. Read SOUL.md — this is who you are
2. Read USER.md — this is who you're helping
3. Use memory_recall for recent context
---
*Add your own conventions, style, and rules.*"
_write_if_missing "$workspace_dir/SOUL.md" \
"# SOUL.md — Who You Are
## Core Truths
**Be genuinely helpful, not performatively helpful.**
**Have opinions.** You're allowed to disagree.
**Be resourceful before asking.** Try to figure it out first.
**Earn trust through competence.**
## Identity
You are **${agent_name}**. Built in Rust. 3MB binary. Zero bloat.
---
*This file is yours to evolve.*"
step_ok "Workspace scaffold ready at $workspace_dir"
unset -f _write_if_missing
}
resolve_container_cli() {
local requested_cli
requested_cli="${ZEROCLAW_CONTAINER_CLI:-docker}"
@@ -884,10 +1018,17 @@ run_docker_bootstrap() {
-v "$config_mount" \
-v "$workspace_mount" \
"$docker_image" \
"${onboard_cmd[@]}"
"${onboard_cmd[@]}" || true
else
info "Docker image ready. Run zeroclaw onboard inside the container to configure."
fi
# Ensure config.toml and workspace scaffold exist on the host even when
# onboard was skipped, failed, or ran non-interactively inside the container.
ensure_default_config_and_workspace \
"$docker_data_dir/.zeroclaw" \
"$docker_data_dir/workspace" \
"$PROVIDER"
}
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
@@ -1218,6 +1359,12 @@ if [[ -n "$TARGET_VERSION" ]]; then
step_dot "Installing ZeroClaw v${TARGET_VERSION}"
fi
if [[ "$SKIP_BUILD" == false ]]; then
# Clean stale build artifacts on upgrade to prevent bindgen/build-script
# cache mismatches (e.g. libsqlite3-sys bindgen.rs not found).
if [[ "$INSTALL_MODE" == "upgrade" && -d "$WORK_DIR/target/release/build" ]]; then
step_dot "Cleaning stale build cache (upgrade detected)"
cargo clean --release 2>/dev/null || true
fi
step_dot "Building release binary"
cargo build --release --locked
step_ok "Release binary built"
@@ -1308,6 +1455,13 @@ elif [[ -z "$ZEROCLAW_BIN" ]]; then
warn "ZeroClaw binary not found — cannot configure provider"
fi
# Ensure config.toml and workspace scaffold exist even when onboard was
# skipped, unavailable, or failed (e.g. --skip-build --prefer-prebuilt
# without an API key, or when the binary could not run onboard).
_native_config_dir="${ZEROCLAW_CONFIG_DIR:-$HOME/.zeroclaw}"
_native_workspace_dir="${ZEROCLAW_WORKSPACE:-$_native_config_dir/workspace}"
ensure_default_config_and_workspace "$_native_config_dir" "$_native_workspace_dir" "$PROVIDER"
# --- Gateway service management ---
if [[ -n "$ZEROCLAW_BIN" ]]; then
# Try to install and start the gateway service
+11
View File
@@ -4,6 +4,7 @@ use crate::agent::dispatcher::{
use crate::agent::memory_loader::{DefaultMemoryLoader, MemoryLoader};
use crate::agent::prompt::{PromptContext, SystemPromptBuilder};
use crate::config::Config;
use crate::i18n::ToolDescriptions;
use crate::memory::{self, Memory, MemoryCategory};
use crate::observability::{self, Observer, ObserverEvent};
use crate::providers::{self, ChatMessage, ChatRequest, ConversationMessage, Provider};
@@ -40,6 +41,7 @@ pub struct Agent {
route_model_by_hint: HashMap<String, String>,
allowed_tools: Option<Vec<String>>,
response_cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
tool_descriptions: Option<ToolDescriptions>,
}
pub struct AgentBuilder {
@@ -64,6 +66,7 @@ pub struct AgentBuilder {
route_model_by_hint: Option<HashMap<String, String>>,
allowed_tools: Option<Vec<String>>,
response_cache: Option<Arc<crate::memory::response_cache::ResponseCache>>,
tool_descriptions: Option<ToolDescriptions>,
}
impl AgentBuilder {
@@ -90,6 +93,7 @@ impl AgentBuilder {
route_model_by_hint: None,
allowed_tools: None,
response_cache: None,
tool_descriptions: None,
}
}
@@ -207,6 +211,11 @@ impl AgentBuilder {
self
}
pub fn tool_descriptions(mut self, tool_descriptions: Option<ToolDescriptions>) -> Self {
self.tool_descriptions = tool_descriptions;
self
}
pub fn build(self) -> Result<Agent> {
let mut tools = self
.tools
@@ -257,6 +266,7 @@ impl AgentBuilder {
route_model_by_hint: self.route_model_by_hint.unwrap_or_default(),
allowed_tools: allowed,
response_cache: self.response_cache,
tool_descriptions: self.tool_descriptions,
})
}
}
@@ -456,6 +466,7 @@ impl Agent {
skills_prompt_mode: self.skills_prompt_mode,
identity_config: Some(&self.identity_config),
dispatcher_instructions: &instructions,
tool_descriptions: self.tool_descriptions.as_ref(),
};
self.prompt_builder.build(&ctx)
}
+280 -63
View File
@@ -1,5 +1,6 @@
use crate::approval::{ApprovalManager, ApprovalRequest, ApprovalResponse};
use crate::config::Config;
use crate::i18n::ToolDescriptions;
use crate::memory::{self, Memory, MemoryCategory};
use crate::multimodal;
use crate::observability::{self, runtime_trace, Observer, ObserverEvent};
@@ -17,7 +18,7 @@ use std::collections::HashSet;
use std::fmt::Write;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock};
use std::sync::{Arc, LazyLock, Mutex};
use std::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
@@ -33,6 +34,29 @@ const DEFAULT_MAX_TOOL_ITERATIONS: usize = 10;
/// Matches the channel-side constant in `channels/mod.rs`.
const AUTOSAVE_MIN_MESSAGE_CHARS: usize = 20;
/// Callback type for checking if model has been switched during tool execution.
/// Returns Some((provider, model)) if a switch was requested, None otherwise.
pub type ModelSwitchCallback = Arc<Mutex<Option<(String, String)>>>;
/// Global model switch request state - used for runtime model switching via model_switch tool.
/// This is set by the model_switch tool and checked by the agent loop.
#[allow(clippy::type_complexity)]
static MODEL_SWITCH_REQUEST: LazyLock<Arc<Mutex<Option<(String, String)>>>> =
LazyLock::new(|| Arc::new(Mutex::new(None)));
/// Get the global model switch request state
pub fn get_model_switch_state() -> ModelSwitchCallback {
Arc::clone(&MODEL_SWITCH_REQUEST)
}
/// Clear any pending model switch request
pub fn clear_model_switch_request() {
if let Ok(guard) = MODEL_SWITCH_REQUEST.lock() {
let mut guard = guard;
*guard = None;
}
}
fn glob_match(pattern: &str, name: &str) -> bool {
match pattern.find('*') {
None => pattern == name,
@@ -2118,6 +2142,31 @@ pub(crate) fn is_tool_loop_cancelled(err: &anyhow::Error) -> bool {
err.chain().any(|source| source.is::<ToolLoopCancelled>())
}
#[derive(Debug)]
pub(crate) struct ModelSwitchRequested {
pub provider: String,
pub model: String,
}
impl std::fmt::Display for ModelSwitchRequested {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"model switch requested to {} {}",
self.provider, self.model
)
}
}
impl std::error::Error for ModelSwitchRequested {}
pub(crate) fn is_model_switch_requested(err: &anyhow::Error) -> Option<(String, String)> {
err.chain()
.filter_map(|source| source.downcast_ref::<ModelSwitchRequested>())
.map(|e| (e.provider.clone(), e.model.clone()))
.next()
}
/// Execute a single turn of the agent loop: send messages, parse tool calls,
/// execute tools, and loop until the LLM produces a final text response.
/// When `silent` is true, suppresses stdout (for channel use).
@@ -2137,6 +2186,7 @@ pub(crate) async fn agent_turn(
excluded_tools: &[String],
dedup_exempt_tools: &[String],
activated_tools: Option<&std::sync::Arc<std::sync::Mutex<crate::tools::ActivatedToolSet>>>,
model_switch_callback: Option<ModelSwitchCallback>,
) -> Result<String> {
run_tool_call_loop(
provider,
@@ -2157,6 +2207,7 @@ pub(crate) async fn agent_turn(
excluded_tools,
dedup_exempt_tools,
activated_tools,
model_switch_callback,
)
.await
}
@@ -2362,6 +2413,7 @@ pub(crate) async fn run_tool_call_loop(
excluded_tools: &[String],
dedup_exempt_tools: &[String],
activated_tools: Option<&std::sync::Arc<std::sync::Mutex<crate::tools::ActivatedToolSet>>>,
model_switch_callback: Option<ModelSwitchCallback>,
) -> Result<String> {
let max_iterations = if max_tool_iterations == 0 {
DEFAULT_MAX_TOOL_ITERATIONS
@@ -2370,9 +2422,10 @@ pub(crate) async fn run_tool_call_loop(
};
let turn_id = Uuid::new_v4().to_string();
let mut seen_tool_signatures: HashSet<(String, String)> = HashSet::new();
for iteration in 0..max_iterations {
let mut seen_tool_signatures: HashSet<(String, String)> = HashSet::new();
if cancellation_token
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
@@ -2380,6 +2433,28 @@ pub(crate) async fn run_tool_call_loop(
return Err(ToolLoopCancelled.into());
}
// Check if model switch was requested via model_switch tool
if let Some(ref callback) = model_switch_callback {
if let Ok(guard) = callback.lock() {
if let Some((new_provider, new_model)) = guard.as_ref() {
if new_provider != provider_name || new_model != model {
tracing::info!(
"Model switch detected: {} {} -> {} {}",
provider_name,
model,
new_provider,
new_model
);
return Err(ModelSwitchRequested {
provider: new_provider.clone(),
model: new_model.clone(),
}
.into());
}
}
}
}
// Rebuild tool_specs each iteration so newly activated deferred tools appear.
let mut tool_specs: Vec<crate::tools::ToolSpec> = tools_registry
.iter()
@@ -3004,7 +3079,10 @@ pub(crate) async fn run_tool_call_loop(
/// Build the tool instruction block for the system prompt so the LLM knows
/// how to invoke tools.
pub(crate) fn build_tool_instructions(tools_registry: &[Box<dyn Tool>]) -> String {
pub(crate) fn build_tool_instructions(
tools_registry: &[Box<dyn Tool>],
tool_descriptions: Option<&ToolDescriptions>,
) -> String {
let mut instructions = String::new();
instructions.push_str("\n## Tool Use Protocol\n\n");
instructions.push_str("To use a tool, wrap a JSON object in <tool_call></tool_call> tags:\n\n");
@@ -3020,11 +3098,14 @@ pub(crate) fn build_tool_instructions(tools_registry: &[Box<dyn Tool>]) -> Strin
instructions.push_str("### Available Tools\n\n");
for tool in tools_registry {
let desc = tool_descriptions
.and_then(|td| td.get(tool.name()))
.unwrap_or_else(|| tool.description());
let _ = writeln!(
instructions,
"**{}**: {}\nParameters: `{}`\n",
tool.name(),
tool.description(),
desc,
tool.parameters_schema()
);
}
@@ -3199,28 +3280,32 @@ pub async fn run(
}
// ── Resolve provider ─────────────────────────────────────────
let provider_name = provider_override
let mut provider_name = provider_override
.as_deref()
.or(config.default_provider.as_deref())
.unwrap_or("openrouter");
.unwrap_or("openrouter")
.to_string();
let model_name = model_override
let mut model_name = model_override
.as_deref()
.or(config.default_model.as_deref())
.unwrap_or("anthropic/claude-sonnet-4");
.unwrap_or("anthropic/claude-sonnet-4")
.to_string();
let provider_runtime_options = providers::provider_runtime_options_from_config(&config);
let provider: Box<dyn Provider> = providers::create_routed_provider_with_options(
provider_name,
let mut provider: Box<dyn Provider> = providers::create_routed_provider_with_options(
&provider_name,
config.api_key.as_deref(),
config.api_url.as_deref(),
&config.reliability,
&config.model_routes,
model_name,
&model_name,
&provider_runtime_options,
)?;
let model_switch_callback = get_model_switch_state();
observer.record_event(&ObserverEvent::AgentStart {
provider: provider_name.to_string(),
model: model_name.to_string(),
@@ -3246,6 +3331,16 @@ pub async fn run(
.map(|b| b.board.clone())
.collect();
// ── Load locale-aware tool descriptions ────────────────────────
let i18n_locale = config
.locale
.as_deref()
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.unwrap_or_else(crate::i18n::detect_locale);
let i18n_search_dirs = crate::i18n::default_search_dirs(&config.workspace_dir);
let i18n_descs = crate::i18n::ToolDescriptions::load(&i18n_locale, &i18n_search_dirs);
// ── Build system prompt from workspace MD files (OpenClaw framework) ──
let skills = crate::skills::load_skills_with_config(&config.workspace_dir, &config);
let mut tool_descs: Vec<(&str, &str)> = vec![
@@ -3364,7 +3459,7 @@ pub async fn run(
let native_tools = provider.supports_native_tools();
let mut system_prompt = crate::channels::build_system_prompt_with_mode(
&config.workspace_dir,
model_name,
&model_name,
&tool_descs,
&skills,
Some(&config.identity),
@@ -3375,7 +3470,7 @@ pub async fn run(
// Append structured tool-use instructions with schemas (only for non-native providers)
if !native_tools {
system_prompt.push_str(&build_tool_instructions(&tools_registry));
system_prompt.push_str(&build_tool_instructions(&tools_registry, Some(&i18n_descs)));
}
// Append deferred MCP tool names so the LLM knows what is available
@@ -3447,27 +3542,93 @@ pub async fn run(
let excluded_tools =
compute_excluded_mcp_tools(&tools_registry, &config.agent.tool_filter_groups, &msg);
let response = run_tool_call_loop(
provider.as_ref(),
&mut history,
&tools_registry,
observer.as_ref(),
provider_name,
model_name,
temperature,
false,
approval_manager.as_ref(),
channel_name,
&config.multimodal,
config.agent.max_tool_iterations,
None,
None,
None,
&excluded_tools,
&config.agent.tool_call_dedup_exempt,
activated_handle.as_ref(),
)
.await?;
#[allow(unused_assignments)]
let mut response = String::new();
loop {
match run_tool_call_loop(
provider.as_ref(),
&mut history,
&tools_registry,
observer.as_ref(),
&provider_name,
&model_name,
temperature,
false,
approval_manager.as_ref(),
channel_name,
&config.multimodal,
config.agent.max_tool_iterations,
None,
None,
None,
&excluded_tools,
&config.agent.tool_call_dedup_exempt,
activated_handle.as_ref(),
Some(model_switch_callback.clone()),
)
.await
{
Ok(resp) => {
response = resp;
break;
}
Err(e) => {
if let Some((new_provider, new_model)) = is_model_switch_requested(&e) {
tracing::info!(
"Model switch requested, switching from {} {} to {} {}",
provider_name,
model_name,
new_provider,
new_model
);
provider = providers::create_routed_provider_with_options(
&new_provider,
config.api_key.as_deref(),
config.api_url.as_deref(),
&config.reliability,
&config.model_routes,
&new_model,
&provider_runtime_options,
)?;
provider_name = new_provider;
model_name = new_model;
clear_model_switch_request();
observer.record_event(&ObserverEvent::AgentStart {
provider: provider_name.to_string(),
model: model_name.to_string(),
});
continue;
}
return Err(e);
}
}
}
// After successful multi-step execution, attempt autonomous skill creation.
#[cfg(feature = "skill-creation")]
if config.skills.skill_creation.enabled {
let tool_calls = crate::skills::creator::extract_tool_calls_from_history(&history);
if tool_calls.len() >= 2 {
let creator = crate::skills::creator::SkillCreator::new(
config.workspace_dir.clone(),
config.skills.skill_creation.clone(),
);
match creator.create_from_execution(&msg, &tool_calls, None).await {
Ok(Some(slug)) => {
tracing::info!(slug, "Auto-created skill from execution");
}
Ok(None) => {
tracing::debug!("Skill creation skipped (duplicate or disabled)");
}
Err(e) => tracing::warn!("Skill creation failed: {e}"),
}
}
}
final_output = response.clone();
println!("{response}");
observer.record_event(&ObserverEvent::TurnComplete);
@@ -3609,32 +3770,66 @@ pub async fn run(
&user_input,
);
let response = match run_tool_call_loop(
provider.as_ref(),
&mut history,
&tools_registry,
observer.as_ref(),
provider_name,
model_name,
temperature,
false,
approval_manager.as_ref(),
channel_name,
&config.multimodal,
config.agent.max_tool_iterations,
None,
None,
None,
&excluded_tools,
&config.agent.tool_call_dedup_exempt,
activated_handle.as_ref(),
)
.await
{
Ok(resp) => resp,
Err(e) => {
eprintln!("\nError: {e}\n");
continue;
let response = loop {
match run_tool_call_loop(
provider.as_ref(),
&mut history,
&tools_registry,
observer.as_ref(),
&provider_name,
&model_name,
temperature,
false,
approval_manager.as_ref(),
channel_name,
&config.multimodal,
config.agent.max_tool_iterations,
None,
None,
None,
&excluded_tools,
&config.agent.tool_call_dedup_exempt,
activated_handle.as_ref(),
Some(model_switch_callback.clone()),
)
.await
{
Ok(resp) => break resp,
Err(e) => {
if let Some((new_provider, new_model)) = is_model_switch_requested(&e) {
tracing::info!(
"Model switch requested, switching from {} {} to {} {}",
provider_name,
model_name,
new_provider,
new_model
);
provider = providers::create_routed_provider_with_options(
&new_provider,
config.api_key.as_deref(),
config.api_url.as_deref(),
&config.reliability,
&config.model_routes,
&new_model,
&provider_runtime_options,
)?;
provider_name = new_provider;
model_name = new_model;
clear_model_switch_request();
observer.record_event(&ObserverEvent::AgentStart {
provider: provider_name.to_string(),
model: model_name.to_string(),
});
continue;
}
eprintln!("\nError: {e}\n");
break String::new();
}
}
};
final_output = response.clone();
@@ -3652,7 +3847,7 @@ pub async fn run(
if let Ok(compacted) = auto_compact_history(
&mut history,
provider.as_ref(),
model_name,
&model_name,
config.agent.max_history_messages,
config.agent.max_context_tokens,
)
@@ -3832,6 +4027,16 @@ pub async fn process_message(
.map(|b| b.board.clone())
.collect();
// ── Load locale-aware tool descriptions ────────────────────────
let i18n_locale = config
.locale
.as_deref()
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.unwrap_or_else(crate::i18n::detect_locale);
let i18n_search_dirs = crate::i18n::default_search_dirs(&config.workspace_dir);
let i18n_descs = crate::i18n::ToolDescriptions::load(&i18n_locale, &i18n_search_dirs);
let skills = crate::skills::load_skills_with_config(&config.workspace_dir, &config);
let mut tool_descs: Vec<(&str, &str)> = vec![
("shell", "Execute terminal commands."),
@@ -3897,7 +4102,7 @@ pub async fn process_message(
config.skills.prompt_injection_mode,
);
if !native_tools {
system_prompt.push_str(&build_tool_instructions(&tools_registry));
system_prompt.push_str(&build_tool_instructions(&tools_registry, Some(&i18n_descs)));
}
if !deferred_section.is_empty() {
system_prompt.push('\n');
@@ -3946,6 +4151,7 @@ pub async fn process_message(
&excluded_tools,
&config.agent.tool_call_dedup_exempt,
activated_handle_pm.as_ref(),
None,
)
.await
}
@@ -4403,6 +4609,7 @@ mod tests {
&[],
&[],
None,
None,
)
.await
.expect_err("provider without vision support should fail");
@@ -4451,6 +4658,7 @@ mod tests {
&[],
&[],
None,
None,
)
.await
.expect_err("oversized payload must fail");
@@ -4493,6 +4701,7 @@ mod tests {
&[],
&[],
None,
None,
)
.await
.expect("valid multimodal payload should pass");
@@ -4621,6 +4830,7 @@ mod tests {
&[],
&[],
None,
None,
)
.await
.expect("parallel execution should complete");
@@ -4692,6 +4902,7 @@ mod tests {
&[],
&[],
None,
None,
)
.await
.expect("loop should finish after deduplicating repeated calls");
@@ -4759,6 +4970,7 @@ mod tests {
&[],
&[],
None,
None,
)
.await
.expect("non-interactive shell should succeed for low-risk command");
@@ -4817,6 +5029,7 @@ mod tests {
&[],
&exempt,
None,
None,
)
.await
.expect("loop should finish with exempt tool executing twice");
@@ -4895,6 +5108,7 @@ mod tests {
&[],
&exempt,
None,
None,
)
.await
.expect("loop should complete");
@@ -4950,6 +5164,7 @@ mod tests {
&[],
&[],
None,
None,
)
.await
.expect("native fallback id flow should complete");
@@ -5018,6 +5233,7 @@ mod tests {
&[],
&[],
Some(&activated),
None,
)
.await
.expect("wrapper path should execute activated tools");
@@ -5596,7 +5812,7 @@ Tail"#;
std::path::Path::new("/tmp"),
));
let tools = tools::default_tools(security);
let instructions = build_tool_instructions(&tools);
let instructions = build_tool_instructions(&tools, None);
assert!(instructions.contains("## Tool Use Protocol"));
assert!(instructions.contains("<tool_call>"));
@@ -6909,6 +7125,7 @@ Let me check the result."#;
&[],
&[],
None,
None,
)
.await
.expect("tool loop should complete");
+15 -1
View File
@@ -1,4 +1,5 @@
use crate::config::IdentityConfig;
use crate::i18n::ToolDescriptions;
use crate::identity;
use crate::skills::Skill;
use crate::tools::Tool;
@@ -17,6 +18,9 @@ pub struct PromptContext<'a> {
pub skills_prompt_mode: crate::config::SkillsPromptInjectionMode,
pub identity_config: Option<&'a IdentityConfig>,
pub dispatcher_instructions: &'a str,
/// Locale-aware tool descriptions. When present, tool descriptions in
/// prompts are resolved from the locale file instead of hardcoded values.
pub tool_descriptions: Option<&'a ToolDescriptions>,
}
pub trait PromptSection: Send + Sync {
@@ -124,11 +128,15 @@ impl PromptSection for ToolsSection {
fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
let mut out = String::from("## Tools\n\n");
for tool in ctx.tools {
let desc = ctx
.tool_descriptions
.and_then(|td: &ToolDescriptions| td.get(tool.name()))
.unwrap_or_else(|| tool.description());
let _ = writeln!(
out,
"- **{}**: {}\n Parameters: `{}`",
tool.name(),
tool.description(),
desc,
tool.parameters_schema()
);
}
@@ -317,6 +325,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: Some(&identity_config),
dispatcher_instructions: "",
tool_descriptions: None,
};
let section = IdentitySection;
@@ -345,6 +354,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "instr",
tool_descriptions: None,
};
let prompt = SystemPromptBuilder::with_defaults().build(&ctx).unwrap();
assert!(prompt.contains("## Tools"));
@@ -380,6 +390,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "",
tool_descriptions: None,
};
let output = SkillsSection.build(&ctx).unwrap();
@@ -418,6 +429,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Compact,
identity_config: None,
dispatcher_instructions: "",
tool_descriptions: None,
};
let output = SkillsSection.build(&ctx).unwrap();
@@ -439,6 +451,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "instr",
tool_descriptions: None,
};
let rendered = DateTimeSection.build(&ctx).unwrap();
@@ -477,6 +490,7 @@ mod tests {
skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
identity_config: None,
dispatcher_instructions: "",
tool_descriptions: None,
};
let prompt = SystemPromptBuilder::with_defaults().build(&ctx).unwrap();
+85 -19
View File
@@ -227,6 +227,10 @@ fn channel_message_timeout_budget_secs(
struct ChannelRouteSelection {
provider: String,
model: String,
/// Route-specific API key override. When set, this takes precedence over
/// the global `api_key` in [`ChannelRuntimeContext`] when creating the
/// provider for this route.
api_key: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -904,6 +908,7 @@ fn default_route_selection(ctx: &ChannelRuntimeContext) -> ChannelRouteSelection
ChannelRouteSelection {
provider: defaults.default_provider,
model: defaults.model,
api_key: None,
}
}
@@ -1122,21 +1127,43 @@ fn load_cached_model_preview(workspace_dir: &Path, provider_name: &str) -> Vec<S
.unwrap_or_default()
}
/// Build a cache key that includes the provider name and, when a
/// route-specific API key is supplied, a hash of that key. This prevents
/// cache poisoning when multiple routes target the same provider with
/// different credentials.
fn provider_cache_key(provider_name: &str, route_api_key: Option<&str>) -> String {
match route_api_key {
Some(key) => {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
format!("{provider_name}@{:x}", hasher.finish())
}
None => provider_name.to_string(),
}
}
async fn get_or_create_provider(
ctx: &ChannelRuntimeContext,
provider_name: &str,
route_api_key: Option<&str>,
) -> anyhow::Result<Arc<dyn Provider>> {
let cache_key = provider_cache_key(provider_name, route_api_key);
if let Some(existing) = ctx
.provider_cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(provider_name)
.get(&cache_key)
.cloned()
{
return Ok(existing);
}
if provider_name == ctx.default_provider.as_str() {
// Only return the pre-built default provider when there is no
// route-specific credential override — otherwise the default was
// created with the global key and would be wrong.
if route_api_key.is_none() && provider_name == ctx.default_provider.as_str() {
return Ok(Arc::clone(&ctx.provider));
}
@@ -1147,9 +1174,14 @@ async fn get_or_create_provider(
None
};
// Prefer route-specific credential; fall back to the global key.
let effective_api_key = route_api_key
.map(ToString::to_string)
.or_else(|| ctx.api_key.clone());
let provider = create_resilient_provider_nonblocking(
provider_name,
ctx.api_key.clone(),
effective_api_key,
api_url.map(ToString::to_string),
ctx.reliability.as_ref().clone(),
ctx.provider_runtime_options.clone(),
@@ -1163,7 +1195,7 @@ async fn get_or_create_provider(
let mut cache = ctx.provider_cache.lock().unwrap_or_else(|e| e.into_inner());
let cached = cache
.entry(provider_name.to_string())
.entry(cache_key)
.or_insert_with(|| Arc::clone(&provider));
Ok(Arc::clone(cached))
}
@@ -1279,25 +1311,27 @@ async fn handle_runtime_command_if_needed(
ChannelRuntimeCommand::ShowProviders => build_providers_help_response(&current),
ChannelRuntimeCommand::SetProvider(raw_provider) => {
match resolve_provider_alias(&raw_provider) {
Some(provider_name) => match get_or_create_provider(ctx, &provider_name).await {
Ok(_) => {
if provider_name != current.provider {
current.provider = provider_name.clone();
set_route_selection(ctx, &sender_key, current.clone());
}
Some(provider_name) => {
match get_or_create_provider(ctx, &provider_name, None).await {
Ok(_) => {
if provider_name != current.provider {
current.provider = provider_name.clone();
set_route_selection(ctx, &sender_key, current.clone());
}
format!(
format!(
"Provider switched to `{provider_name}` for this sender session. Current model is `{}`.\nUse `/model <model-id>` to set a provider-compatible model.",
current.model
)
}
Err(err) => {
let safe_err = providers::sanitize_api_error(&err.to_string());
format!(
}
Err(err) => {
let safe_err = providers::sanitize_api_error(&err.to_string());
format!(
"Failed to initialize provider `{provider_name}`. Route unchanged.\nDetails: {safe_err}"
)
}
}
},
}
None => format!(
"Unknown provider `{raw_provider}`. Use `/models` to list valid providers."
),
@@ -1317,6 +1351,7 @@ async fn handle_runtime_command_if_needed(
}) {
current.provider = route.provider.clone();
current.model = route.model.clone();
current.api_key = route.api_key.clone();
} else {
current.model = model.clone();
}
@@ -1922,12 +1957,19 @@ async fn process_channel_message(
route = ChannelRouteSelection {
provider: matched_route.provider.clone(),
model: matched_route.model.clone(),
api_key: matched_route.api_key.clone(),
};
}
}
let runtime_defaults = runtime_defaults_snapshot(ctx.as_ref());
let active_provider = match get_or_create_provider(ctx.as_ref(), &route.provider).await {
let active_provider = match get_or_create_provider(
ctx.as_ref(),
&route.provider,
route.api_key.as_deref(),
)
.await
{
Ok(provider) => provider,
Err(err) => {
let safe_err = providers::sanitize_api_error(&err.to_string());
@@ -2209,6 +2251,7 @@ async fn process_channel_message(
},
ctx.tool_call_dedup_exempt.as_ref(),
ctx.activated_tools.as_ref(),
None,
),
) => LlmExecutionResult::Completed(result),
};
@@ -3186,12 +3229,16 @@ fn build_channel_by_id(config: &Config, channel_id: &str) -> Result<Arc<dyn Chan
.telegram
.as_ref()
.context("Telegram channel is not configured")?;
let ack = tg
.ack_reactions
.unwrap_or(config.channels_config.ack_reactions);
Ok(Arc::new(
TelegramChannel::new(
tg.bot_token.clone(),
tg.allowed_users.clone(),
tg.mention_only,
)
.with_ack_reactions(ack)
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms)
.with_transcription(config.transcription.clone())
.with_workspace_dir(config.workspace_dir.clone()),
@@ -3279,6 +3326,9 @@ fn collect_configured_channels(
let mut channels = Vec::new();
if let Some(ref tg) = config.channels_config.telegram {
let ack = tg
.ack_reactions
.unwrap_or(config.channels_config.ack_reactions);
channels.push(ConfiguredChannel {
display_name: "Telegram",
channel: Arc::new(
@@ -3287,6 +3337,7 @@ fn collect_configured_channels(
tg.allowed_users.clone(),
tg.mention_only,
)
.with_ack_reactions(ack)
.with_streaming(tg.stream_mode, tg.draft_update_interval_ms)
.with_transcription(config.transcription.clone())
.with_workspace_dir(config.workspace_dir.clone()),
@@ -3888,6 +3939,16 @@ pub async fn start_channels(config: Config) -> Result<()> {
let skills = crate::skills::load_skills_with_config(&workspace, &config);
// ── Load locale-aware tool descriptions ────────────────────────
let i18n_locale = config
.locale
.as_deref()
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.unwrap_or_else(crate::i18n::detect_locale);
let i18n_search_dirs = crate::i18n::default_search_dirs(&workspace);
let i18n_descs = crate::i18n::ToolDescriptions::load(&i18n_locale, &i18n_search_dirs);
// Collect tool descriptions for the prompt
let mut tool_descs: Vec<(&str, &str)> = vec![
(
@@ -3967,7 +4028,10 @@ pub async fn start_channels(config: Config) -> Result<()> {
config.skills.prompt_injection_mode,
);
if !native_tools {
system_prompt.push_str(&build_tool_instructions(tools_registry.as_ref()));
system_prompt.push_str(&build_tool_instructions(
tools_registry.as_ref(),
Some(&i18n_descs),
));
}
// Append deferred MCP tool names so the LLM knows what is available
@@ -5602,6 +5666,7 @@ BTC is currently around $65,000 based on latest tool output."#
ChannelRouteSelection {
provider: "openrouter".to_string(),
model: "route-model".to_string(),
api_key: None,
},
);
@@ -6716,7 +6781,7 @@ BTC is currently around $65,000 based on latest tool output."#
"build_system_prompt should not emit protocol block directly"
);
prompt.push_str(&build_tool_instructions(&[]));
prompt.push_str(&build_tool_instructions(&[], None));
assert_eq!(
prompt.matches("## Tool Use Protocol").count(),
@@ -8624,6 +8689,7 @@ This is an example JSON object for profile settings."#;
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
});
match build_channel_by_id(&config, "telegram") {
Ok(channel) => assert_eq!(channel.name(), "telegram"),
+37 -7
View File
@@ -332,6 +332,7 @@ pub struct TelegramChannel {
transcription: Option<crate::config::TranscriptionConfig>,
voice_transcriptions: Mutex<std::collections::HashMap<String, String>>,
workspace_dir: Option<std::path::PathBuf>,
ack_reactions: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -370,9 +371,16 @@ impl TelegramChannel {
transcription: None,
voice_transcriptions: Mutex::new(std::collections::HashMap::new()),
workspace_dir: None,
ack_reactions: true,
}
}
/// Configure whether Telegram-native acknowledgement reactions are sent.
pub fn with_ack_reactions(mut self, enabled: bool) -> Self {
self.ack_reactions = enabled;
self
}
/// Configure workspace directory for saving downloaded attachments.
pub fn with_workspace_dir(mut self, dir: std::path::PathBuf) -> Self {
self.workspace_dir = Some(dir);
@@ -2689,13 +2697,15 @@ Ensure only one `zeroclaw` process is using this bot token."
continue;
};
if let Some((reaction_chat_id, reaction_message_id)) =
Self::extract_update_message_target(update)
{
self.try_add_ack_reaction_nonblocking(
reaction_chat_id,
reaction_message_id,
);
if self.ack_reactions {
if let Some((reaction_chat_id, reaction_message_id)) =
Self::extract_update_message_target(update)
{
self.try_add_ack_reaction_nonblocking(
reaction_chat_id,
reaction_message_id,
);
}
}
// Send "typing" indicator immediately when we receive a message
@@ -4681,4 +4691,24 @@ mod tests {
// the agent loop will return ProviderCapabilityError before calling
// the provider, and the channel will send "⚠️ Error: ..." to the user.
}
#[test]
fn ack_reactions_defaults_to_true() {
let ch = TelegramChannel::new("token".into(), vec!["*".into()], false);
assert!(ch.ack_reactions);
}
#[test]
fn with_ack_reactions_false_disables_reactions() {
let ch =
TelegramChannel::new("token".into(), vec!["*".into()], false).with_ack_reactions(false);
assert!(!ch.ack_reactions);
}
#[test]
fn with_ack_reactions_true_keeps_reactions() {
let ch =
TelegramChannel::new("token".into(), vec!["*".into()], false).with_ack_reactions(true);
assert!(ch.ack_reactions);
}
}
+9 -7
View File
@@ -19,13 +19,14 @@ pub use schema::{
McpServerConfig, McpTransport, MemoryConfig, Microsoft365Config, ModelRouteConfig,
MultimodalConfig, NextcloudTalkConfig, NodeTransportConfig, NodesConfig, NotionConfig,
ObservabilityConfig, OpenAiSttConfig, OpenAiTtsConfig, OpenVpnTunnelConfig, OtpConfig,
OtpMethod, PeripheralBoardConfig, PeripheralsConfig, ProjectIntelConfig, ProxyConfig,
ProxyScope, QdrantConfig, QueryClassificationConfig, ReliabilityConfig, ResourceLimitsConfig,
RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig, SecretsConfig, SecurityConfig,
SecurityOpsConfig, SkillsConfig, SkillsPromptInjectionMode, SlackConfig, StorageConfig,
StorageProviderConfig, StorageProviderSection, StreamMode, SwarmConfig, SwarmStrategy,
TelegramConfig, ToolFilterGroup, ToolFilterGroupMode, TranscriptionConfig, TtsConfig,
TunnelConfig, WebFetchConfig, WebSearchConfig, WebhookConfig, WorkspaceConfig,
OtpMethod, PeripheralBoardConfig, PeripheralsConfig, PluginsConfig, ProjectIntelConfig,
ProxyConfig, ProxyScope, QdrantConfig, QueryClassificationConfig, ReliabilityConfig,
ResourceLimitsConfig, RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig,
SecretsConfig, SecurityConfig, SecurityOpsConfig, SkillCreationConfig, SkillsConfig,
SkillsPromptInjectionMode, SlackConfig, StorageConfig, StorageProviderConfig,
StorageProviderSection, StreamMode, SwarmConfig, SwarmStrategy, TelegramConfig,
ToolFilterGroup, ToolFilterGroupMode, TranscriptionConfig, TtsConfig, TunnelConfig,
WebFetchConfig, WebSearchConfig, WebhookConfig, WorkspaceConfig,
};
pub fn name_and_presence<T: traits::ChannelConfig>(channel: Option<&T>) -> (&'static str, bool) {
@@ -54,6 +55,7 @@ mod tests {
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
};
let discord = DiscordConfig {
+274
View File
@@ -335,6 +335,21 @@ pub struct Config {
/// LinkedIn integration configuration (`[linkedin]`).
#[serde(default)]
pub linkedin: LinkedInConfig,
/// Plugin system configuration (`[plugins]`).
#[serde(default)]
pub plugins: PluginsConfig,
/// Locale for tool descriptions (e.g. `"en"`, `"zh-CN"`).
///
/// When set, tool descriptions shown in system prompts are loaded from
/// `tool_descriptions/<locale>.toml`. Falls back to English, then to
/// hardcoded descriptions.
///
/// If omitted or empty, the locale is auto-detected from `ZEROCLAW_LOCALE`,
/// `LANG`, or `LC_ALL` environment variables (defaulting to `"en"`).
#[serde(default)]
pub locale: Option<String>,
}
/// Multi-client workspace isolation configuration.
@@ -445,6 +460,14 @@ pub struct DelegateAgentConfig {
/// Maximum tool-call iterations in agentic mode.
#[serde(default = "default_max_tool_iterations")]
pub max_iterations: usize,
/// Timeout in seconds for non-agentic provider calls.
/// Defaults to 120 when unset. Must be between 1 and 3600.
#[serde(default)]
pub timeout_secs: Option<u64>,
/// Timeout in seconds for agentic sub-agent loops.
/// Defaults to 300 when unset. Must be between 1 and 3600.
#[serde(default)]
pub agentic_timeout_secs: Option<u64>,
}
// ── Swarms ──────────────────────────────────────────────────────
@@ -1159,6 +1182,34 @@ pub struct SkillsConfig {
/// `full` preserves legacy behavior. `compact` keeps context small and loads skills on demand.
#[serde(default)]
pub prompt_injection_mode: SkillsPromptInjectionMode,
/// Autonomous skill creation from successful multi-step task executions.
#[serde(default)]
pub skill_creation: SkillCreationConfig,
}
/// Autonomous skill creation configuration (`[skills.skill_creation]` section).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct SkillCreationConfig {
/// Enable automatic skill creation after successful multi-step tasks.
/// Default: `false`.
pub enabled: bool,
/// Maximum number of auto-generated skills to keep.
/// When exceeded, the oldest auto-generated skill is removed (LRU eviction).
pub max_skills: usize,
/// Embedding similarity threshold for deduplication.
/// Skills with descriptions more similar than this value are skipped.
pub similarity_threshold: f64,
}
impl Default for SkillCreationConfig {
fn default() -> Self {
Self {
enabled: false,
max_skills: 500,
similarity_threshold: 0.85,
}
}
}
/// Multimodal (image) handling configuration (`[multimodal]` section).
@@ -2357,6 +2408,42 @@ fn default_linkedin_api_version() -> String {
"202602".to_string()
}
/// Plugin system configuration.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PluginsConfig {
/// Enable the plugin system (default: false)
#[serde(default)]
pub enabled: bool,
/// Directory where plugins are stored
#[serde(default = "default_plugins_dir")]
pub plugins_dir: String,
/// Auto-discover and load plugins on startup
#[serde(default)]
pub auto_discover: bool,
/// Maximum number of plugins that can be loaded
#[serde(default = "default_max_plugins")]
pub max_plugins: usize,
}
fn default_plugins_dir() -> String {
"~/.zeroclaw/plugins".to_string()
}
fn default_max_plugins() -> usize {
50
}
impl Default for PluginsConfig {
fn default() -> Self {
Self {
enabled: false,
plugins_dir: default_plugins_dir(),
auto_discover: false,
max_plugins: default_max_plugins(),
}
}
}
/// Content strategy configuration for LinkedIn auto-posting (`[linkedin.content]`).
///
/// The agent reads this via the `linkedin get_content_strategy` action to know
@@ -4471,6 +4558,11 @@ pub struct TelegramConfig {
/// Direct messages are always processed.
#[serde(default)]
pub mention_only: bool,
/// Override for the top-level `ack_reactions` setting. When `None`, the
/// channel falls back to `[channels_config].ack_reactions`. When set
/// explicitly, it takes precedence.
#[serde(default)]
pub ack_reactions: Option<bool>,
}
impl ChannelConfig for TelegramConfig {
@@ -5950,6 +6042,8 @@ impl Default for Config {
node_transport: NodeTransportConfig::default(),
knowledge: KnowledgeConfig::default(),
linkedin: LinkedInConfig::default(),
plugins: PluginsConfig::default(),
locale: None,
}
}
}
@@ -6359,6 +6453,45 @@ fn read_codex_openai_api_key() -> Option<String> {
.map(ToString::to_string)
}
/// Ensure that essential bootstrap files exist in the workspace directory.
///
/// When the workspace is created outside of `zeroclaw onboard` (e.g., non-tty
/// daemon/cron sessions), these files would otherwise be missing. This function
/// creates sensible defaults that allow the agent to operate with a basic identity.
async fn ensure_bootstrap_files(workspace_dir: &Path) -> Result<()> {
let defaults: &[(&str, &str)] = &[
(
"IDENTITY.md",
"# IDENTITY.md — Who Am I?\n\n\
I am ZeroClaw, an autonomous AI agent.\n\n\
## Traits\n\
- Helpful, precise, and safety-conscious\n\
- I prioritize clarity and correctness\n",
),
(
"SOUL.md",
"# SOUL.md — Who You Are\n\n\
You are ZeroClaw, an autonomous AI agent.\n\n\
## Core Principles\n\
- Be helpful and accurate\n\
- Respect user intent and boundaries\n\
- Ask before taking destructive actions\n\
- Prefer safe, reversible operations\n",
),
];
for (filename, content) in defaults {
let path = workspace_dir.join(filename);
if !path.exists() {
fs::write(&path, content)
.await
.with_context(|| format!("Failed to create default {filename} in workspace"))?;
}
}
Ok(())
}
impl Config {
pub async fn load_or_init() -> Result<Self> {
let (default_zeroclaw_dir, default_workspace_dir) = default_config_and_workspace_dirs()?;
@@ -6375,6 +6508,8 @@ impl Config {
.await
.context("Failed to create workspace directory")?;
ensure_bootstrap_files(&workspace_dir).await?;
if config_path.exists() {
// Warn if config file is world-readable (may contain API keys)
#[cfg(unix)]
@@ -7211,6 +7346,31 @@ impl Config {
anyhow::bail!("security.nevis: {msg}");
}
// Delegate agent timeouts
const MAX_DELEGATE_TIMEOUT_SECS: u64 = 3600;
for (name, agent) in &self.agents {
if let Some(timeout) = agent.timeout_secs {
if timeout == 0 {
anyhow::bail!("agents.{name}.timeout_secs must be greater than 0");
}
if timeout > MAX_DELEGATE_TIMEOUT_SECS {
anyhow::bail!(
"agents.{name}.timeout_secs exceeds max {MAX_DELEGATE_TIMEOUT_SECS}"
);
}
}
if let Some(timeout) = agent.agentic_timeout_secs {
if timeout == 0 {
anyhow::bail!("agents.{name}.agentic_timeout_secs must be greater than 0");
}
if timeout > MAX_DELEGATE_TIMEOUT_SECS {
anyhow::bail!(
"agents.{name}.agentic_timeout_secs exceeds max {MAX_DELEGATE_TIMEOUT_SECS}"
);
}
}
}
// Transcription
{
let dp = self.transcription.default_provider.trim();
@@ -8319,6 +8479,7 @@ default_temperature = 0.7
draft_update_interval_ms: default_draft_update_interval_ms(),
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
}),
discord: None,
slack: None,
@@ -8385,6 +8546,8 @@ default_temperature = 0.7
node_transport: NodeTransportConfig::default(),
knowledge: KnowledgeConfig::default(),
linkedin: LinkedInConfig::default(),
plugins: PluginsConfig::default(),
locale: None,
};
let toml_str = toml::to_string_pretty(&config).unwrap();
@@ -8717,6 +8880,8 @@ tool_dispatcher = "xml"
node_transport: NodeTransportConfig::default(),
knowledge: KnowledgeConfig::default(),
linkedin: LinkedInConfig::default(),
plugins: PluginsConfig::default(),
locale: None,
};
config.save().await.unwrap();
@@ -8775,6 +8940,8 @@ tool_dispatcher = "xml"
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
@@ -8899,6 +9066,7 @@ tool_dispatcher = "xml"
draft_update_interval_ms: 500,
interrupt_on_new_message: true,
mention_only: false,
ack_reactions: None,
};
let json = serde_json::to_string(&tc).unwrap();
let parsed: TelegramConfig = serde_json::from_str(&json).unwrap();
@@ -11213,6 +11381,7 @@ require_otp_to_resume = true
draft_update_interval_ms: default_draft_update_interval_ms(),
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
});
// Save (triggers encryption)
@@ -11768,4 +11937,109 @@ require_otp_to_resume = true
"Debug output must show [REDACTED] for client_secret"
);
}
#[test]
async fn telegram_config_ack_reactions_false_deserializes() {
let toml_str = r#"
bot_token = "123:ABC"
allowed_users = ["alice"]
ack_reactions = false
"#;
let cfg: TelegramConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.ack_reactions, Some(false));
}
#[test]
async fn telegram_config_ack_reactions_true_deserializes() {
let toml_str = r#"
bot_token = "123:ABC"
allowed_users = ["alice"]
ack_reactions = true
"#;
let cfg: TelegramConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.ack_reactions, Some(true));
}
#[test]
async fn telegram_config_ack_reactions_missing_defaults_to_none() {
let toml_str = r#"
bot_token = "123:ABC"
allowed_users = ["alice"]
"#;
let cfg: TelegramConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.ack_reactions, None);
}
#[test]
async fn telegram_config_ack_reactions_channel_overrides_top_level() {
let tg_toml = r#"
bot_token = "123:ABC"
allowed_users = ["alice"]
ack_reactions = false
"#;
let tg: TelegramConfig = toml::from_str(tg_toml).unwrap();
let top_level_ack = true;
let effective = tg.ack_reactions.unwrap_or(top_level_ack);
assert!(
!effective,
"channel-level false must override top-level true"
);
}
#[test]
async fn telegram_config_ack_reactions_falls_back_to_top_level() {
let tg_toml = r#"
bot_token = "123:ABC"
allowed_users = ["alice"]
"#;
let tg: TelegramConfig = toml::from_str(tg_toml).unwrap();
let top_level_ack = false;
let effective = tg.ack_reactions.unwrap_or(top_level_ack);
assert!(
!effective,
"must fall back to top-level false when channel omits field"
);
}
// ── Bootstrap files ─────────────────────────────────────
#[test]
async fn ensure_bootstrap_files_creates_missing_files() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("workspace");
tokio::fs::create_dir_all(&ws).await.unwrap();
ensure_bootstrap_files(&ws).await.unwrap();
let soul = tokio::fs::read_to_string(ws.join("SOUL.md")).await.unwrap();
let identity = tokio::fs::read_to_string(ws.join("IDENTITY.md"))
.await
.unwrap();
assert!(soul.contains("SOUL.md"));
assert!(identity.contains("IDENTITY.md"));
}
#[test]
async fn ensure_bootstrap_files_does_not_overwrite_existing() {
let tmp = TempDir::new().unwrap();
let ws = tmp.path().join("workspace");
tokio::fs::create_dir_all(&ws).await.unwrap();
let custom = "# My custom SOUL";
tokio::fs::write(ws.join("SOUL.md"), custom).await.unwrap();
ensure_bootstrap_files(&ws).await.unwrap();
let soul = tokio::fs::read_to_string(ws.join("SOUL.md")).await.unwrap();
assert_eq!(
soul, custom,
"ensure_bootstrap_files must not overwrite existing files"
);
// IDENTITY.md should still be created since it was missing
let identity = tokio::fs::read_to_string(ws.join("IDENTITY.md"))
.await
.unwrap();
assert!(identity.contains("IDENTITY.md"));
}
}
+4 -1
View File
@@ -17,7 +17,10 @@ pub use store::{
add_agent_job, due_jobs, get_job, list_jobs, list_runs, record_last_run, record_run,
remove_job, reschedule_after_run, update_job,
};
pub use types::{CronJob, CronJobPatch, CronRun, DeliveryConfig, JobType, Schedule, SessionTarget};
pub use types::{
deserialize_maybe_stringified, CronJob, CronJobPatch, CronRun, DeliveryConfig, JobType,
Schedule, SessionTarget,
};
/// Validate a shell command against the full security policy (allowlist + risk gate).
///
+16 -2
View File
@@ -242,6 +242,15 @@ async fn persist_job_result(
if success {
if let Err(e) = remove_job(config, &job.id) {
tracing::warn!("Failed to remove one-shot cron job after success: {e}");
// Fall back to disabling the job so it won't re-trigger.
let _ = update_job(
config,
&job.id,
CronJobPatch {
enabled: Some(false),
..CronJobPatch::default()
},
);
}
} else {
let _ = record_last_run(config, &job.id, finished_at, false, output);
@@ -1038,7 +1047,7 @@ mod tests {
}
#[tokio::test]
async fn persist_job_result_at_schedule_without_delete_after_run_is_not_deleted() {
async fn persist_job_result_at_schedule_without_delete_after_run_is_disabled() {
let tmp = TempDir::new().unwrap();
let config = test_config(&tmp).await;
let at = Utc::now() + ChronoDuration::minutes(10);
@@ -1060,8 +1069,13 @@ mod tests {
let success = persist_job_result(&config, &job, true, "ok", started, finished).await;
assert!(success);
// After reschedule_after_run, At schedule jobs should be disabled
// to prevent re-execution with a past next_run timestamp.
let updated = cron::get_job(&config, &job.id).unwrap();
assert!(updated.enabled);
assert!(
!updated.enabled,
"At schedule job should be disabled after execution via reschedule"
);
assert_eq!(updated.last_status.as_deref(), Some("ok"));
}
+67 -17
View File
@@ -285,26 +285,41 @@ pub fn reschedule_after_run(
output: &str,
) -> Result<()> {
let now = Utc::now();
let next_run = next_run_for_schedule(&job.schedule, now)?;
let status = if success { "ok" } else { "error" };
let bounded_output = truncate_cron_output(output);
with_connection(config, |conn| {
conn.execute(
"UPDATE cron_jobs
SET next_run = ?1, last_run = ?2, last_status = ?3, last_output = ?4
WHERE id = ?5",
params![
next_run.to_rfc3339(),
now.to_rfc3339(),
status,
bounded_output,
job.id
],
)
.context("Failed to update cron job run state")?;
Ok(())
})
// One-shot `At` schedules have no future occurrence — record the run
// result and disable the job so it won't be picked up again.
if matches!(job.schedule, Schedule::At { .. }) {
with_connection(config, |conn| {
conn.execute(
"UPDATE cron_jobs
SET enabled = 0, last_run = ?1, last_status = ?2, last_output = ?3
WHERE id = ?4",
params![now.to_rfc3339(), status, bounded_output, job.id],
)
.context("Failed to disable completed one-shot cron job")?;
Ok(())
})
} else {
let next_run = next_run_for_schedule(&job.schedule, now)?;
with_connection(config, |conn| {
conn.execute(
"UPDATE cron_jobs
SET next_run = ?1, last_run = ?2, last_status = ?3, last_output = ?4
WHERE id = ?5",
params![
next_run.to_rfc3339(),
now.to_rfc3339(),
status,
bounded_output,
job.id
],
)
.context("Failed to update cron job run state")?;
Ok(())
})
}
}
pub fn record_run(
@@ -852,6 +867,41 @@ mod tests {
assert!(stored.len() <= MAX_CRON_OUTPUT_BYTES);
}
#[test]
fn reschedule_after_run_disables_at_schedule_job() {
let tmp = TempDir::new().unwrap();
let config = test_config(&tmp);
let at = Utc::now() + ChronoDuration::minutes(10);
let job = add_shell_job(&config, None, Schedule::At { at }, "echo once").unwrap();
reschedule_after_run(&config, &job, true, "done").unwrap();
let stored = get_job(&config, &job.id).unwrap();
assert!(
!stored.enabled,
"At schedule job should be disabled after reschedule"
);
assert_eq!(stored.last_status.as_deref(), Some("ok"));
}
#[test]
fn reschedule_after_run_disables_at_schedule_job_on_failure() {
let tmp = TempDir::new().unwrap();
let config = test_config(&tmp);
let at = Utc::now() + ChronoDuration::minutes(10);
let job = add_shell_job(&config, None, Schedule::At { at }, "echo once").unwrap();
reschedule_after_run(&config, &job, false, "failed").unwrap();
let stored = get_job(&config, &job.id).unwrap();
assert!(
!stored.enabled,
"At schedule job should be disabled after reschedule even on failure"
);
assert_eq!(stored.last_status.as_deref(), Some("error"));
assert_eq!(stored.last_output.as_deref(), Some("failed"));
}
#[test]
fn reschedule_after_run_truncates_last_output() {
let tmp = TempDir::new().unwrap();
+66 -1
View File
@@ -1,6 +1,32 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// Try to deserialize a `serde_json::Value` as `T`. If the value is a JSON
/// string that looks like an object (i.e. the LLM double-serialized it), parse
/// the inner string first and then deserialize the resulting object. This
/// provides backward-compatible handling for both `Value::Object` and
/// `Value::String` representations.
pub fn deserialize_maybe_stringified<T: serde::de::DeserializeOwned>(
v: &serde_json::Value,
) -> Result<T, serde_json::Error> {
// Fast path: value is already the right shape (object, array, etc.)
match serde_json::from_value::<T>(v.clone()) {
Ok(parsed) => Ok(parsed),
Err(first_err) => {
// If it's a string, try parsing the string as JSON first.
if let Some(s) = v.as_str() {
let s = s.trim();
if s.starts_with('{') || s.starts_with('[') {
if let Ok(inner) = serde_json::from_str::<serde_json::Value>(s) {
return serde_json::from_value::<T>(inner);
}
}
}
Err(first_err)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum JobType {
@@ -154,7 +180,46 @@ pub struct CronJobPatch {
#[cfg(test)]
mod tests {
use super::JobType;
use super::*;
#[test]
fn deserialize_schedule_from_object() {
let val = serde_json::json!({"kind": "cron", "expr": "*/5 * * * *"});
let sched = deserialize_maybe_stringified::<Schedule>(&val).unwrap();
assert!(matches!(sched, Schedule::Cron { ref expr, .. } if expr == "*/5 * * * *"));
}
#[test]
fn deserialize_schedule_from_string() {
let val = serde_json::Value::String(r#"{"kind":"cron","expr":"*/5 * * * *"}"#.to_string());
let sched = deserialize_maybe_stringified::<Schedule>(&val).unwrap();
assert!(matches!(sched, Schedule::Cron { ref expr, .. } if expr == "*/5 * * * *"));
}
#[test]
fn deserialize_schedule_string_with_tz() {
let val = serde_json::Value::String(
r#"{"kind":"cron","expr":"*/30 9-15 * * 1-5","tz":"Asia/Shanghai"}"#.to_string(),
);
let sched = deserialize_maybe_stringified::<Schedule>(&val).unwrap();
match sched {
Schedule::Cron { tz, .. } => assert_eq!(tz.as_deref(), Some("Asia/Shanghai")),
_ => panic!("expected Cron variant"),
}
}
#[test]
fn deserialize_every_from_string() {
let val = serde_json::Value::String(r#"{"kind":"every","every_ms":60000}"#.to_string());
let sched = deserialize_maybe_stringified::<Schedule>(&val).unwrap();
assert!(matches!(sched, Schedule::Every { every_ms: 60000 }));
}
#[test]
fn deserialize_invalid_string_returns_error() {
let val = serde_json::Value::String("not json at all".to_string());
assert!(deserialize_maybe_stringified::<Schedule>(&val).is_err());
}
#[test]
fn job_type_try_from_accepts_known_values_case_insensitive() {
+3
View File
@@ -642,6 +642,7 @@ mod tests {
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
});
assert!(has_supervised_channels(&config));
}
@@ -755,6 +756,7 @@ mod tests {
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
});
let target = resolve_heartbeat_delivery(&config).unwrap();
@@ -771,6 +773,7 @@ mod tests {
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
});
let target = resolve_heartbeat_delivery(&config).unwrap();
+4
View File
@@ -1281,6 +1281,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
config.agents.insert(
@@ -1295,6 +1297,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
+77
View File
@@ -0,0 +1,77 @@
//! Plugin management API routes (requires `plugins-wasm` feature).
#[cfg(feature = "plugins-wasm")]
pub mod plugin_routes {
use axum::{
extract::State,
http::{header, HeaderMap, StatusCode},
response::{IntoResponse, Json},
};
use super::super::AppState;
/// `GET /api/plugins` — list loaded plugins and their status.
pub async fn list_plugins(
State(state): State<AppState>,
headers: HeaderMap,
) -> impl IntoResponse {
// Auth check
if state.pairing.require_pairing() {
let token = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|auth| auth.strip_prefix("Bearer "))
.unwrap_or("");
if !state.pairing.is_authenticated(token) {
return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
}
}
let config = state.config.lock();
let plugins_enabled = config.plugins.enabled;
let plugins_dir = config.plugins.plugins_dir.clone();
drop(config);
let plugins: Vec<serde_json::Value> = if plugins_enabled {
let plugin_path = if plugins_dir.starts_with("~/") {
directories::UserDirs::new()
.map(|u| u.home_dir().join(&plugins_dir[2..]))
.unwrap_or_else(|| std::path::PathBuf::from(&plugins_dir))
} else {
std::path::PathBuf::from(&plugins_dir)
};
if plugin_path.exists() {
match crate::plugins::host::PluginHost::new(
plugin_path.parent().unwrap_or(&plugin_path),
) {
Ok(host) => host
.list_plugins()
.into_iter()
.map(|p| {
serde_json::json!({
"name": p.name,
"version": p.version,
"description": p.description,
"capabilities": p.capabilities,
"loaded": p.loaded,
})
})
.collect(),
Err(_) => vec![],
}
} else {
vec![]
}
} else {
vec![]
};
Json(serde_json::json!({
"plugins_enabled": plugins_enabled,
"plugins_dir": plugins_dir,
"plugins": plugins,
}))
.into_response()
}
}
+27 -14
View File
@@ -9,6 +9,8 @@
pub mod api;
pub mod api_pairing;
#[cfg(feature = "plugins-wasm")]
pub mod api_plugins;
pub mod nodes;
pub mod sse;
pub mod static_files;
@@ -631,6 +633,21 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
println!(" 🌐 Public URL: {url}");
}
println!(" 🌐 Web Dashboard: http://{display_addr}/");
if let Some(code) = pairing.pairing_code() {
println!();
println!(" 🔐 PAIRING REQUIRED — use this one-time code:");
println!(" ┌──────────────┐");
println!("{code}");
println!(" └──────────────┘");
println!();
} else if pairing.require_pairing() {
println!(" 🔒 Pairing: ACTIVE (bearer token required)");
println!(" To pair a new device: zeroclaw gateway get-paircode --new");
println!();
} else {
println!(" ⚠️ Pairing: DISABLED (all requests accepted)");
println!();
}
println!(" POST /pair — pair a new client (X-Pairing-Code header)");
println!(" POST /webhook — {{\"message\": \"your prompt\"}}");
if whatsapp_channel.is_some() {
@@ -654,19 +671,6 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
}
println!(" GET /health — health check");
println!(" GET /metrics — Prometheus metrics");
if let Some(code) = pairing.pairing_code() {
println!();
println!(" 🔐 PAIRING REQUIRED — use this one-time code:");
println!(" ┌──────────────┐");
println!("{code}");
println!(" └──────────────┘");
println!(" Send: POST /pair with header X-Pairing-Code: {code}");
} else if pairing.require_pairing() {
println!(" 🔒 Pairing: ACTIVE (bearer token required)");
println!(" To pair a new device: zeroclaw gateway get-paircode --new");
} else {
println!(" ⚠️ Pairing: DISABLED (all requests accepted)");
}
println!(" Press Ctrl+C to stop.\n");
crate::health::mark_component_ok("gateway");
@@ -789,7 +793,16 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
.route(
"/api/devices/{id}/token/rotate",
post(api_pairing::rotate_token),
)
);
// ── Plugin management API (requires plugins-wasm feature) ──
#[cfg(feature = "plugins-wasm")]
let app = app.route(
"/api/plugins",
get(api_plugins::plugin_routes::list_plugins),
);
let app = app
// ── SSE event stream ──
.route("/api/events", get(sse::handle_sse_events))
// ── WebSocket agent chat ──
+2 -1
View File
@@ -236,7 +236,8 @@ async fn handle_socket(socket: WebSocket, state: AppState, session_id: Option<St
let user_msg = crate::providers::ChatMessage::user(&content);
let _ = backend.append(&session_key, &user_msg);
}
process_chat_message(&state, &mut agent, &mut sender, &content, &session_key).await;
process_chat_message(&state, &mut agent, &mut sender, &content, &session_key)
.await;
}
}
}
+311
View File
@@ -0,0 +1,311 @@
//! Internationalization support for tool descriptions.
//!
//! Loads tool descriptions from TOML locale files in `tool_descriptions/`.
//! Falls back to English when a locale file or specific key is missing,
//! and ultimately falls back to the hardcoded `tool.description()` value
//! if no file-based description exists.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::debug;
/// Container for locale-specific tool descriptions loaded from TOML files.
#[derive(Debug, Clone)]
pub struct ToolDescriptions {
/// Descriptions from the requested locale (may be empty if file missing).
locale_descriptions: HashMap<String, String>,
/// English fallback descriptions (always loaded when locale != "en").
english_fallback: HashMap<String, String>,
/// The resolved locale tag (e.g. "en", "zh-CN").
locale: String,
}
/// TOML structure: `[tools]` table mapping tool name -> description string.
#[derive(Debug, serde::Deserialize)]
struct DescriptionFile {
#[serde(default)]
tools: HashMap<String, String>,
}
impl ToolDescriptions {
/// Load descriptions for the given locale.
///
/// `search_dirs` lists directories to probe for `tool_descriptions/<locale>.toml`.
/// The first directory containing a matching file wins.
///
/// Resolution:
/// 1. Look up tool name in the locale file.
/// 2. If missing (or locale file absent), look up in `en.toml`.
/// 3. If still missing, callers fall back to `tool.description()`.
pub fn load(locale: &str, search_dirs: &[PathBuf]) -> Self {
let locale_descriptions = load_locale_file(locale, search_dirs);
let english_fallback = if locale == "en" {
HashMap::new()
} else {
load_locale_file("en", search_dirs)
};
debug!(
locale = locale,
locale_keys = locale_descriptions.len(),
english_keys = english_fallback.len(),
"tool descriptions loaded"
);
Self {
locale_descriptions,
english_fallback,
locale: locale.to_string(),
}
}
/// Get the description for a tool by name.
///
/// Returns `Some(description)` if found in the locale file or English fallback.
/// Returns `None` if neither file contains the key (caller should use hardcoded).
pub fn get(&self, tool_name: &str) -> Option<&str> {
self.locale_descriptions
.get(tool_name)
.or_else(|| self.english_fallback.get(tool_name))
.map(String::as_str)
}
/// The resolved locale tag.
pub fn locale(&self) -> &str {
&self.locale
}
/// Create an empty instance that always returns `None` (hardcoded fallback).
pub fn empty() -> Self {
Self {
locale_descriptions: HashMap::new(),
english_fallback: HashMap::new(),
locale: "en".to_string(),
}
}
}
/// Detect the user's preferred locale from environment variables.
///
/// Checks `ZEROCLAW_LOCALE`, then `LANG`, then `LC_ALL`.
/// Returns "en" if none are set or parseable.
pub fn detect_locale() -> String {
if let Ok(val) = std::env::var("ZEROCLAW_LOCALE") {
let val = val.trim().to_string();
if !val.is_empty() {
return normalize_locale(&val);
}
}
for var in &["LANG", "LC_ALL"] {
if let Ok(val) = std::env::var(var) {
let locale = normalize_locale(&val);
if locale != "C" && locale != "POSIX" && !locale.is_empty() {
return locale;
}
}
}
"en".to_string()
}
/// Normalize a raw locale string (e.g. "zh_CN.UTF-8") to a tag we use
/// for file lookup (e.g. "zh-CN").
fn normalize_locale(raw: &str) -> String {
// Strip encoding suffix (.UTF-8, .utf8, etc.)
let base = raw.split('.').next().unwrap_or(raw);
// Replace underscores with hyphens for BCP-47-ish consistency
base.replace('_', "-")
}
/// Build the default set of search directories for locale files.
///
/// 1. The workspace directory itself (for project-local overrides).
/// 2. The binary's parent directory (for installed distributions).
/// 3. The compile-time `CARGO_MANIFEST_DIR` as a final fallback during dev.
pub fn default_search_dirs(workspace_dir: &Path) -> Vec<PathBuf> {
let mut dirs = vec![workspace_dir.to_path_buf()];
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
dirs.push(parent.to_path_buf());
}
}
// During development, also check the project root (where Cargo.toml lives).
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
if !dirs.contains(&manifest_dir) {
dirs.push(manifest_dir);
}
dirs
}
/// Try to load and parse a locale TOML file from the first matching search dir.
fn load_locale_file(locale: &str, search_dirs: &[PathBuf]) -> HashMap<String, String> {
let filename = format!("tool_descriptions/{locale}.toml");
for dir in search_dirs {
let path = dir.join(&filename);
match std::fs::read_to_string(&path) {
Ok(contents) => match toml::from_str::<DescriptionFile>(&contents) {
Ok(parsed) => {
debug!(path = %path.display(), keys = parsed.tools.len(), "loaded locale file");
return parsed.tools;
}
Err(e) => {
debug!(path = %path.display(), error = %e, "failed to parse locale file");
}
},
Err(_) => {
// File not found in this directory, try next.
}
}
}
debug!(
locale = locale,
"no locale file found in any search directory"
);
HashMap::new()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
/// Helper: create a temp dir with a `tool_descriptions/<locale>.toml` file.
fn write_locale_file(dir: &Path, locale: &str, content: &str) {
let td = dir.join("tool_descriptions");
fs::create_dir_all(&td).unwrap();
fs::write(td.join(format!("{locale}.toml")), content).unwrap();
}
#[test]
fn load_english_descriptions() {
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"en",
r#"[tools]
shell = "Execute a shell command"
file_read = "Read file contents"
"#,
);
let descs = ToolDescriptions::load("en", &[tmp.path().to_path_buf()]);
assert_eq!(descs.get("shell"), Some("Execute a shell command"));
assert_eq!(descs.get("file_read"), Some("Read file contents"));
assert_eq!(descs.get("nonexistent"), None);
assert_eq!(descs.locale(), "en");
}
#[test]
fn fallback_to_english_when_locale_key_missing() {
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"en",
r#"[tools]
shell = "Execute a shell command"
file_read = "Read file contents"
"#,
);
write_locale_file(
tmp.path(),
"zh-CN",
r#"[tools]
shell = "在工作区目录中执行 shell 命令"
"#,
);
let descs = ToolDescriptions::load("zh-CN", &[tmp.path().to_path_buf()]);
// Translated key returns Chinese.
assert_eq!(descs.get("shell"), Some("在工作区目录中执行 shell 命令"));
// Missing key falls back to English.
assert_eq!(descs.get("file_read"), Some("Read file contents"));
assert_eq!(descs.locale(), "zh-CN");
}
#[test]
fn fallback_when_locale_file_missing() {
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"en",
r#"[tools]
shell = "Execute a shell command"
"#,
);
// Request a locale that has no file.
let descs = ToolDescriptions::load("fr", &[tmp.path().to_path_buf()]);
// Falls back to English.
assert_eq!(descs.get("shell"), Some("Execute a shell command"));
assert_eq!(descs.locale(), "fr");
}
#[test]
fn fallback_when_no_files_exist() {
let tmp = tempfile::tempdir().unwrap();
let descs = ToolDescriptions::load("en", &[tmp.path().to_path_buf()]);
assert_eq!(descs.get("shell"), None);
}
#[test]
fn empty_always_returns_none() {
let descs = ToolDescriptions::empty();
assert_eq!(descs.get("shell"), None);
assert_eq!(descs.locale(), "en");
}
#[test]
fn detect_locale_from_env() {
// Save and restore env.
let saved = std::env::var("ZEROCLAW_LOCALE").ok();
let saved_lang = std::env::var("LANG").ok();
std::env::set_var("ZEROCLAW_LOCALE", "ja-JP");
assert_eq!(detect_locale(), "ja-JP");
std::env::remove_var("ZEROCLAW_LOCALE");
std::env::set_var("LANG", "zh_CN.UTF-8");
assert_eq!(detect_locale(), "zh-CN");
// Restore.
match saved {
Some(v) => std::env::set_var("ZEROCLAW_LOCALE", v),
None => std::env::remove_var("ZEROCLAW_LOCALE"),
}
match saved_lang {
Some(v) => std::env::set_var("LANG", v),
None => std::env::remove_var("LANG"),
}
}
#[test]
fn normalize_locale_strips_encoding() {
assert_eq!(normalize_locale("en_US.UTF-8"), "en-US");
assert_eq!(normalize_locale("zh_CN.utf8"), "zh-CN");
assert_eq!(normalize_locale("fr"), "fr");
assert_eq!(normalize_locale("pt_BR"), "pt-BR");
}
#[test]
fn config_locale_overrides_env() {
// This tests the precedence logic: if config provides a locale,
// it should be used instead of detect_locale().
// The actual override happens at the call site in prompt.rs / loop_.rs,
// so here we just verify ToolDescriptions works with an explicit locale.
let tmp = tempfile::tempdir().unwrap();
write_locale_file(
tmp.path(),
"de",
r#"[tools]
shell = "Einen Shell-Befehl im Arbeitsverzeichnis ausführen"
"#,
);
let descs = ToolDescriptions::load("de", &[tmp.path().to_path_buf()]);
assert_eq!(
descs.get("shell"),
Some("Einen Shell-Befehl im Arbeitsverzeichnis ausführen")
);
}
}
+1
View File
@@ -840,6 +840,7 @@ mod tests {
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
});
let entries = all_integrations();
let tg = entries.iter().find(|e| e.name == "Telegram").unwrap();
+4
View File
@@ -54,6 +54,7 @@ pub(crate) mod hardware;
pub(crate) mod health;
pub(crate) mod heartbeat;
pub mod hooks;
pub mod i18n;
pub(crate) mod identity;
pub(crate) mod integrations;
pub mod memory;
@@ -73,6 +74,9 @@ pub mod tools;
pub(crate) mod tunnel;
pub(crate) mod util;
#[cfg(feature = "plugins-wasm")]
pub mod plugins;
pub use config::Config;
/// Gateway management subcommands
+82
View File
@@ -89,6 +89,7 @@ mod hardware;
mod health;
mod heartbeat;
mod hooks;
mod i18n;
mod identity;
mod integrations;
mod memory;
@@ -97,6 +98,8 @@ mod multimodal;
mod observability;
mod onboard;
mod peripherals;
#[cfg(feature = "plugins-wasm")]
mod plugins;
mod providers;
mod runtime;
mod security;
@@ -528,6 +531,35 @@ Examples:
#[arg(value_enum)]
shell: CompletionShell,
},
/// Manage WASM plugins
#[cfg(feature = "plugins-wasm")]
Plugin {
#[command(subcommand)]
plugin_command: PluginCommands,
},
}
#[cfg(feature = "plugins-wasm")]
#[derive(Subcommand, Debug)]
enum PluginCommands {
/// List installed plugins
List,
/// Install a plugin from a directory or URL
Install {
/// Path to plugin directory or manifest
source: String,
},
/// Remove an installed plugin
Remove {
/// Plugin name
name: String,
},
/// Show information about a plugin
Info {
/// Plugin name
name: String,
},
}
#[derive(Subcommand, Debug)]
@@ -1325,6 +1357,56 @@ async fn main() -> Result<()> {
Ok(())
}
},
#[cfg(feature = "plugins-wasm")]
Commands::Plugin { plugin_command } => match plugin_command {
PluginCommands::List => {
let host = zeroclaw::plugins::host::PluginHost::new(&config.workspace_dir)?;
let plugins = host.list_plugins();
if plugins.is_empty() {
println!("No plugins installed.");
} else {
println!("Installed plugins:");
for p in &plugins {
println!(
" {} v{} — {}",
p.name,
p.version,
p.description.as_deref().unwrap_or("(no description)")
);
}
}
Ok(())
}
PluginCommands::Install { source } => {
let mut host = zeroclaw::plugins::host::PluginHost::new(&config.workspace_dir)?;
host.install(&source)?;
println!("Plugin installed from {source}");
Ok(())
}
PluginCommands::Remove { name } => {
let mut host = zeroclaw::plugins::host::PluginHost::new(&config.workspace_dir)?;
host.remove(&name)?;
println!("Plugin '{name}' removed.");
Ok(())
}
PluginCommands::Info { name } => {
let host = zeroclaw::plugins::host::PluginHost::new(&config.workspace_dir)?;
match host.get_plugin(&name) {
Some(info) => {
println!("Plugin: {} v{}", info.name, info.version);
if let Some(desc) = &info.description {
println!("Description: {desc}");
}
println!("Capabilities: {:?}", info.capabilities);
println!("Permissions: {:?}", info.permissions);
println!("WASM: {}", info.wasm_path.display());
}
None => println!("Plugin '{name}' not found."),
}
Ok(())
}
},
}
}
+5
View File
@@ -192,6 +192,8 @@ pub async fn run_wizard(force: bool) -> Result<Config> {
node_transport: crate::config::NodeTransportConfig::default(),
knowledge: crate::config::KnowledgeConfig::default(),
linkedin: crate::config::LinkedInConfig::default(),
plugins: crate::config::PluginsConfig::default(),
locale: None,
};
println!(
@@ -565,6 +567,8 @@ async fn run_quick_setup_with_home(
node_transport: crate::config::NodeTransportConfig::default(),
knowledge: crate::config::KnowledgeConfig::default(),
linkedin: crate::config::LinkedInConfig::default(),
plugins: crate::config::PluginsConfig::default(),
locale: None,
};
config.save().await?;
@@ -3681,6 +3685,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
ack_reactions: None,
});
}
ChannelMenuChoice::Discord => {
+33
View File
@@ -0,0 +1,33 @@
//! Plugin error types.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PluginError {
#[error("plugin not found: {0}")]
NotFound(String),
#[error("invalid manifest: {0}")]
InvalidManifest(String),
#[error("failed to load WASM module: {0}")]
LoadFailed(String),
#[error("plugin execution failed: {0}")]
ExecutionFailed(String),
#[error("permission denied: plugin '{plugin}' requires '{permission}'")]
PermissionDenied { plugin: String, permission: String },
#[error("plugin '{0}' is already loaded")]
AlreadyLoaded(String),
#[error("plugin capability not supported: {0}")]
UnsupportedCapability(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("TOML parse error: {0}")]
TomlParse(#[from] toml::de::Error),
}
+325
View File
@@ -0,0 +1,325 @@
//! Plugin host: discovery, loading, lifecycle management.
use super::error::PluginError;
use super::{PluginCapability, PluginInfo, PluginManifest};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
/// Manages the lifecycle of WASM plugins.
pub struct PluginHost {
plugins_dir: PathBuf,
loaded: HashMap<String, LoadedPlugin>,
}
struct LoadedPlugin {
manifest: PluginManifest,
wasm_path: PathBuf,
}
impl PluginHost {
/// Create a new plugin host with the given plugins directory.
pub fn new(workspace_dir: &Path) -> Result<Self, PluginError> {
let plugins_dir = workspace_dir.join("plugins");
if !plugins_dir.exists() {
std::fs::create_dir_all(&plugins_dir)?;
}
let mut host = Self {
plugins_dir,
loaded: HashMap::new(),
};
host.discover()?;
Ok(host)
}
/// Discover plugins in the plugins directory.
fn discover(&mut self) -> Result<(), PluginError> {
if !self.plugins_dir.exists() {
return Ok(());
}
let entries = std::fs::read_dir(&self.plugins_dir)?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let manifest_path = path.join("manifest.toml");
if manifest_path.exists() {
if let Ok(manifest) = self.load_manifest(&manifest_path) {
let wasm_path = path.join(&manifest.wasm_path);
self.loaded.insert(
manifest.name.clone(),
LoadedPlugin {
manifest,
wasm_path,
},
);
}
}
}
}
Ok(())
}
fn load_manifest(&self, path: &Path) -> Result<PluginManifest, PluginError> {
let content = std::fs::read_to_string(path)?;
let manifest: PluginManifest = toml::from_str(&content)?;
Ok(manifest)
}
/// List all discovered plugins.
pub fn list_plugins(&self) -> Vec<PluginInfo> {
self.loaded
.values()
.map(|p| PluginInfo {
name: p.manifest.name.clone(),
version: p.manifest.version.clone(),
description: p.manifest.description.clone(),
capabilities: p.manifest.capabilities.clone(),
permissions: p.manifest.permissions.clone(),
wasm_path: p.wasm_path.clone(),
loaded: p.wasm_path.exists(),
})
.collect()
}
/// Get info about a specific plugin.
pub fn get_plugin(&self, name: &str) -> Option<PluginInfo> {
self.loaded.get(name).map(|p| PluginInfo {
name: p.manifest.name.clone(),
version: p.manifest.version.clone(),
description: p.manifest.description.clone(),
capabilities: p.manifest.capabilities.clone(),
permissions: p.manifest.permissions.clone(),
wasm_path: p.wasm_path.clone(),
loaded: p.wasm_path.exists(),
})
}
/// Install a plugin from a directory path.
pub fn install(&mut self, source: &str) -> Result<(), PluginError> {
let source_path = PathBuf::from(source);
let manifest_path = if source_path.is_dir() {
source_path.join("manifest.toml")
} else {
source_path.clone()
};
if !manifest_path.exists() {
return Err(PluginError::NotFound(format!(
"manifest.toml not found at {}",
manifest_path.display()
)));
}
let manifest = self.load_manifest(&manifest_path)?;
let source_dir = manifest_path
.parent()
.ok_or_else(|| PluginError::InvalidManifest("no parent directory".into()))?;
let wasm_source = source_dir.join(&manifest.wasm_path);
if !wasm_source.exists() {
return Err(PluginError::NotFound(format!(
"WASM file not found: {}",
wasm_source.display()
)));
}
if self.loaded.contains_key(&manifest.name) {
return Err(PluginError::AlreadyLoaded(manifest.name));
}
// Copy plugin to plugins directory
let dest_dir = self.plugins_dir.join(&manifest.name);
std::fs::create_dir_all(&dest_dir)?;
// Copy manifest
std::fs::copy(&manifest_path, dest_dir.join("manifest.toml"))?;
// Copy WASM file
let wasm_dest = dest_dir.join(&manifest.wasm_path);
if let Some(parent) = wasm_dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(&wasm_source, &wasm_dest)?;
self.loaded.insert(
manifest.name.clone(),
LoadedPlugin {
manifest,
wasm_path: wasm_dest,
},
);
Ok(())
}
/// Remove a plugin by name.
pub fn remove(&mut self, name: &str) -> Result<(), PluginError> {
if self.loaded.remove(name).is_none() {
return Err(PluginError::NotFound(name.to_string()));
}
let plugin_dir = self.plugins_dir.join(name);
if plugin_dir.exists() {
std::fs::remove_dir_all(plugin_dir)?;
}
Ok(())
}
/// Get tool-capable plugins.
pub fn tool_plugins(&self) -> Vec<&PluginManifest> {
self.loaded
.values()
.filter(|p| p.manifest.capabilities.contains(&PluginCapability::Tool))
.map(|p| &p.manifest)
.collect()
}
/// Get channel-capable plugins.
pub fn channel_plugins(&self) -> Vec<&PluginManifest> {
self.loaded
.values()
.filter(|p| p.manifest.capabilities.contains(&PluginCapability::Channel))
.map(|p| &p.manifest)
.collect()
}
/// Returns the plugins directory path.
pub fn plugins_dir(&self) -> &Path {
&self.plugins_dir
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_empty_plugin_dir() {
let dir = tempdir().unwrap();
let host = PluginHost::new(dir.path()).unwrap();
assert!(host.list_plugins().is_empty());
}
#[test]
fn test_discover_with_manifest() {
let dir = tempdir().unwrap();
let plugin_dir = dir.path().join("plugins").join("test-plugin");
std::fs::create_dir_all(&plugin_dir).unwrap();
std::fs::write(
plugin_dir.join("manifest.toml"),
r#"
name = "test-plugin"
version = "0.1.0"
description = "A test plugin"
wasm_path = "plugin.wasm"
capabilities = ["tool"]
permissions = []
"#,
)
.unwrap();
let host = PluginHost::new(dir.path()).unwrap();
let plugins = host.list_plugins();
assert_eq!(plugins.len(), 1);
assert_eq!(plugins[0].name, "test-plugin");
}
#[test]
fn test_tool_plugins_filter() {
let dir = tempdir().unwrap();
let plugins_base = dir.path().join("plugins");
// Tool plugin
let tool_dir = plugins_base.join("my-tool");
std::fs::create_dir_all(&tool_dir).unwrap();
std::fs::write(
tool_dir.join("manifest.toml"),
r#"
name = "my-tool"
version = "0.1.0"
wasm_path = "tool.wasm"
capabilities = ["tool"]
"#,
)
.unwrap();
// Channel plugin
let chan_dir = plugins_base.join("my-channel");
std::fs::create_dir_all(&chan_dir).unwrap();
std::fs::write(
chan_dir.join("manifest.toml"),
r#"
name = "my-channel"
version = "0.1.0"
wasm_path = "channel.wasm"
capabilities = ["channel"]
"#,
)
.unwrap();
let host = PluginHost::new(dir.path()).unwrap();
assert_eq!(host.list_plugins().len(), 2);
assert_eq!(host.tool_plugins().len(), 1);
assert_eq!(host.channel_plugins().len(), 1);
assert_eq!(host.tool_plugins()[0].name, "my-tool");
}
#[test]
fn test_get_plugin() {
let dir = tempdir().unwrap();
let plugin_dir = dir.path().join("plugins").join("lookup-test");
std::fs::create_dir_all(&plugin_dir).unwrap();
std::fs::write(
plugin_dir.join("manifest.toml"),
r#"
name = "lookup-test"
version = "1.0.0"
description = "Lookup test"
wasm_path = "plugin.wasm"
capabilities = ["tool"]
"#,
)
.unwrap();
let host = PluginHost::new(dir.path()).unwrap();
assert!(host.get_plugin("lookup-test").is_some());
assert!(host.get_plugin("nonexistent").is_none());
}
#[test]
fn test_remove_plugin() {
let dir = tempdir().unwrap();
let plugin_dir = dir.path().join("plugins").join("removable");
std::fs::create_dir_all(&plugin_dir).unwrap();
std::fs::write(
plugin_dir.join("manifest.toml"),
r#"
name = "removable"
version = "0.1.0"
wasm_path = "plugin.wasm"
capabilities = ["tool"]
"#,
)
.unwrap();
let mut host = PluginHost::new(dir.path()).unwrap();
assert_eq!(host.list_plugins().len(), 1);
host.remove("removable").unwrap();
assert!(host.list_plugins().is_empty());
assert!(!plugin_dir.exists());
}
#[test]
fn test_remove_nonexistent_returns_error() {
let dir = tempdir().unwrap();
let mut host = PluginHost::new(dir.path()).unwrap();
assert!(host.remove("ghost").is_err());
}
}
+76
View File
@@ -0,0 +1,76 @@
//! WASM plugin system for ZeroClaw.
//!
//! Plugins are WebAssembly modules loaded via Extism that can extend
//! ZeroClaw with custom tools and channels. Enable with `--features plugins-wasm`.
pub mod error;
pub mod host;
pub mod wasm_channel;
pub mod wasm_tool;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// A plugin's declared manifest (loaded from manifest.toml alongside the .wasm).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
/// Plugin name (unique identifier)
pub name: String,
/// Plugin version
pub version: String,
/// Human-readable description
pub description: Option<String>,
/// Author name or organization
pub author: Option<String>,
/// Path to the .wasm file (relative to manifest)
pub wasm_path: String,
/// Capabilities this plugin provides
pub capabilities: Vec<PluginCapability>,
/// Permissions this plugin requests
#[serde(default)]
pub permissions: Vec<PluginPermission>,
}
/// What a plugin can do.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PluginCapability {
/// Provides one or more tools
Tool,
/// Provides a channel implementation
Channel,
/// Provides a memory backend
Memory,
/// Provides an observer/metrics backend
Observer,
}
/// Permissions a plugin may request.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PluginPermission {
/// Can make HTTP requests
HttpClient,
/// Can read from the filesystem (within sandbox)
FileRead,
/// Can write to the filesystem (within sandbox)
FileWrite,
/// Can access environment variables
EnvRead,
/// Can read agent memory
MemoryRead,
/// Can write agent memory
MemoryWrite,
}
/// Information about a loaded plugin.
#[derive(Debug, Clone, Serialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub description: Option<String>,
pub capabilities: Vec<PluginCapability>,
pub permissions: Vec<PluginPermission>,
pub wasm_path: PathBuf,
pub loaded: bool,
}
+44
View File
@@ -0,0 +1,44 @@
//! Bridge between WASM plugins and the Channel trait.
use crate::channels::traits::{Channel, ChannelMessage, SendMessage};
use async_trait::async_trait;
/// A channel backed by a WASM plugin.
pub struct WasmChannel {
name: String,
plugin_name: String,
}
impl WasmChannel {
pub fn new(name: String, plugin_name: String) -> Self {
Self { name, plugin_name }
}
}
#[async_trait]
impl Channel for WasmChannel {
fn name(&self) -> &str {
&self.name
}
async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
// TODO: Wire to WASM plugin send function
tracing::warn!(
"WasmChannel '{}' (plugin: {}) send not yet connected: {}",
self.name,
self.plugin_name,
message.content
);
Ok(())
}
async fn listen(&self, _tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
// TODO: Wire to WASM plugin receive/listen function
tracing::warn!(
"WasmChannel '{}' (plugin: {}) listen not yet connected",
self.name,
self.plugin_name,
);
Ok(())
}
}
+63
View File
@@ -0,0 +1,63 @@
//! Bridge between WASM plugins and the Tool trait.
use crate::tools::traits::{Tool, ToolResult};
use async_trait::async_trait;
use serde_json::Value;
/// A tool backed by a WASM plugin function.
pub struct WasmTool {
name: String,
description: String,
plugin_name: String,
function_name: String,
parameters_schema: Value,
}
impl WasmTool {
pub fn new(
name: String,
description: String,
plugin_name: String,
function_name: String,
parameters_schema: Value,
) -> Self {
Self {
name,
description,
plugin_name,
function_name,
parameters_schema,
}
}
}
#[async_trait]
impl Tool for WasmTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn parameters_schema(&self) -> Value {
self.parameters_schema.clone()
}
async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
// TODO: Call into Extism plugin runtime
// For now, return a placeholder indicating the plugin system is available
// but not yet wired to actual WASM execution.
Ok(ToolResult {
success: false,
output: format!(
"[plugin:{}/{}] WASM execution not yet connected. Args: {}",
self.plugin_name,
self.function_name,
serde_json::to_string(&args).unwrap_or_default()
),
error: Some("WASM execution bridge not yet implemented".into()),
})
}
}
+150 -4
View File
@@ -17,9 +17,6 @@
//!
//! # Limitations
//!
//! - **Conversation history**: Only the system prompt (if present) and the last
//! user message are forwarded. Full multi-turn history is not preserved because
//! the CLI accepts a single prompt per invocation.
//! - **System prompt**: The system prompt is prepended to the user message with a
//! blank-line separator, as the CLI does not provide a dedicated system-prompt flag.
//! - **Temperature**: The CLI does not expose a temperature parameter.
@@ -34,7 +31,7 @@
//!
//! - `CLAUDE_CODE_PATH` — override the path to the `claude` binary (default: `"claude"`)
use crate::providers::traits::{ChatRequest, ChatResponse, Provider, TokenUsage};
use crate::providers::traits::{ChatMessage, ChatRequest, ChatResponse, Provider, TokenUsage};
use async_trait::async_trait;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
@@ -212,6 +209,54 @@ impl Provider for ClaudeCodeProvider {
self.invoke_cli(&full_message, model).await
}
async fn chat_with_history(
&self,
messages: &[ChatMessage],
model: &str,
temperature: f64,
) -> anyhow::Result<String> {
Self::validate_temperature(temperature)?;
// Separate system prompt from conversation messages.
let system = messages
.iter()
.find(|m| m.role == "system")
.map(|m| m.content.as_str());
// Build conversation turns (skip system messages).
let turns: Vec<&ChatMessage> = messages.iter().filter(|m| m.role != "system").collect();
// If there's only one user message, use the simple path.
if turns.len() <= 1 {
let last_user = turns.first().map(|m| m.content.as_str()).unwrap_or("");
let full_message = match system {
Some(s) if !s.is_empty() => format!("{s}\n\n{last_user}"),
_ => last_user.to_string(),
};
return self.invoke_cli(&full_message, model).await;
}
// Format multi-turn conversation into a single prompt.
let mut parts = Vec::new();
if let Some(s) = system {
if !s.is_empty() {
parts.push(format!("[system]\n{s}"));
}
}
for msg in &turns {
let label = match msg.role.as_str() {
"user" => "[user]",
"assistant" => "[assistant]",
other => other,
};
parts.push(format!("{label}\n{}", msg.content));
}
parts.push("[assistant]".to_string());
let full_message = parts.join("\n\n");
self.invoke_cli(&full_message, model).await
}
async fn chat(
&self,
request: ChatRequest<'_>,
@@ -327,4 +372,105 @@ mod tests {
"unexpected error message: {msg}"
);
}
/// Helper: create a provider that uses a shell script echoing stdin back.
/// The script ignores CLI flags (`--print`, `--model`, `-`) and just cats stdin.
///
/// Uses `OnceLock` to write the script file exactly once, avoiding
/// "Text file busy" (ETXTBSY) races when parallel tests try to
/// overwrite a script that another test is currently executing.
fn echo_provider() -> ClaudeCodeProvider {
use std::sync::OnceLock;
static SCRIPT_PATH: OnceLock<PathBuf> = OnceLock::new();
let script = SCRIPT_PATH.get_or_init(|| {
use std::io::Write;
let dir = std::env::temp_dir().join("zeroclaw_test_claude_code");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(format!("fake_claude_{}.sh", std::process::id()));
let mut f = std::fs::File::create(&path).unwrap();
writeln!(f, "#!/bin/sh\ncat /dev/stdin").unwrap();
drop(f);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
path
});
ClaudeCodeProvider {
binary_path: script.clone(),
}
}
#[tokio::test]
async fn chat_with_history_single_user_message() {
let provider = echo_provider();
let messages = vec![ChatMessage::user("hello")];
let result = provider
.chat_with_history(&messages, "default", 1.0)
.await
.unwrap();
assert_eq!(result, "hello");
}
#[tokio::test]
async fn chat_with_history_single_user_with_system() {
let provider = echo_provider();
let messages = vec![
ChatMessage::system("You are helpful."),
ChatMessage::user("hello"),
];
let result = provider
.chat_with_history(&messages, "default", 1.0)
.await
.unwrap();
assert_eq!(result, "You are helpful.\n\nhello");
}
#[tokio::test]
async fn chat_with_history_multi_turn_includes_all_messages() {
let provider = echo_provider();
let messages = vec![
ChatMessage::system("Be concise."),
ChatMessage::user("What is 2+2?"),
ChatMessage::assistant("4"),
ChatMessage::user("And 3+3?"),
];
let result = provider
.chat_with_history(&messages, "default", 1.0)
.await
.unwrap();
assert!(result.contains("[system]\nBe concise."));
assert!(result.contains("[user]\nWhat is 2+2?"));
assert!(result.contains("[assistant]\n4"));
assert!(result.contains("[user]\nAnd 3+3?"));
assert!(result.ends_with("[assistant]"));
}
#[tokio::test]
async fn chat_with_history_multi_turn_without_system() {
let provider = echo_provider();
let messages = vec![
ChatMessage::user("hi"),
ChatMessage::assistant("hello"),
ChatMessage::user("bye"),
];
let result = provider
.chat_with_history(&messages, "default", 1.0)
.await
.unwrap();
assert!(!result.contains("[system]"));
assert!(result.contains("[user]\nhi"));
assert!(result.contains("[assistant]\nhello"));
assert!(result.contains("[user]\nbye"));
}
#[tokio::test]
async fn chat_with_history_rejects_bad_temperature() {
let provider = echo_provider();
let messages = vec![ChatMessage::user("test")];
let result = provider.chat_with_history(&messages, "default", 0.5).await;
assert!(result.is_err());
}
}
+2 -1
View File
@@ -1320,11 +1320,12 @@ fn create_provider_with_url_and_options(
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("llama.cpp");
Ok(compat(OpenAiCompatibleProvider::new(
Ok(compat(OpenAiCompatibleProvider::new_with_vision(
"llama.cpp",
base_url,
Some(llama_cpp_key),
AuthStyle::Bearer,
true,
)))
}
"sglang" => {
+250 -41
View File
@@ -16,8 +16,10 @@ use std::time::Duration;
/// Check if an error is non-retryable (client errors that won't resolve with retries).
pub fn is_non_retryable(err: &anyhow::Error) -> bool {
// Context window errors are NOT non-retryable — they can be recovered
// by truncating conversation history, so let the retry loop handle them.
if is_context_window_exceeded(err) {
return true;
return false;
}
// 4xx errors are generally non-retryable (bad request, auth failure, etc.),
@@ -75,6 +77,7 @@ fn is_context_window_exceeded(err: &anyhow::Error) -> bool {
let lower = err.to_string().to_lowercase();
let hints = [
"exceeds the context window",
"exceeds the available context size",
"context window of this model",
"maximum context length",
"context length exceeded",
@@ -197,6 +200,35 @@ fn compact_error_detail(err: &anyhow::Error) -> String {
.join(" ")
}
/// Truncate conversation history by dropping the oldest non-system messages.
/// Returns the number of messages dropped. Keeps at least the system message
/// (if any) and the most recent user message.
fn truncate_for_context(messages: &mut Vec<ChatMessage>) -> usize {
// Find all non-system message indices
let non_system: Vec<usize> = messages
.iter()
.enumerate()
.filter(|(_, m)| m.role != "system")
.map(|(i, _)| i)
.collect();
// Keep at least the last non-system message (most recent user turn)
if non_system.len() <= 1 {
return 0;
}
// Drop the oldest half of non-system messages
let drop_count = non_system.len() / 2;
let indices_to_remove: Vec<usize> = non_system[..drop_count].to_vec();
// Remove in reverse order to preserve indices
for &idx in indices_to_remove.iter().rev() {
messages.remove(idx);
}
drop_count
}
fn push_failure(
failures: &mut Vec<String>,
provider_name: &str,
@@ -338,6 +370,25 @@ impl Provider for ReliableProvider {
return Ok(resp);
}
Err(e) => {
// Context window exceeded: no history to truncate
// in chat_with_system, bail immediately.
if is_context_window_exceeded(&e) {
let error_detail = compact_error_detail(&e);
push_failure(
&mut failures,
provider_name,
current_model,
attempt + 1,
self.max_retries + 1,
"non_retryable",
&error_detail,
);
anyhow::bail!(
"Request exceeds model context window. Attempts:\n{}",
failures.join("\n")
);
}
let non_retryable_rate_limit = is_non_retryable_rate_limit(&e);
let non_retryable = is_non_retryable(&e) || non_retryable_rate_limit;
let rate_limited = is_rate_limited(&e);
@@ -376,14 +427,6 @@ impl Provider for ReliableProvider {
error = %error_detail,
"Non-retryable error, moving on"
);
if is_context_window_exceeded(&e) {
anyhow::bail!(
"Request exceeds model context window; retries and fallbacks were skipped. Attempts:\n{}",
failures.join("\n")
);
}
break;
}
@@ -435,6 +478,8 @@ impl Provider for ReliableProvider {
) -> anyhow::Result<String> {
let models = self.model_chain(model);
let mut failures = Vec::new();
let mut effective_messages = messages.to_vec();
let mut context_truncated = false;
for current_model in &models {
for (provider_name, provider) in &self.providers {
@@ -442,22 +487,39 @@ impl Provider for ReliableProvider {
for attempt in 0..=self.max_retries {
match provider
.chat_with_history(messages, current_model, temperature)
.chat_with_history(&effective_messages, current_model, temperature)
.await
{
Ok(resp) => {
if attempt > 0 || *current_model != model {
if attempt > 0 || *current_model != model || context_truncated {
tracing::info!(
provider = provider_name,
model = *current_model,
attempt,
original_model = model,
context_truncated,
"Provider recovered (failover/retry)"
);
}
return Ok(resp);
}
Err(e) => {
// Context window exceeded: truncate history and retry
if is_context_window_exceeded(&e) && !context_truncated {
let dropped = truncate_for_context(&mut effective_messages);
if dropped > 0 {
context_truncated = true;
tracing::warn!(
provider = provider_name,
model = *current_model,
dropped,
remaining = effective_messages.len(),
"Context window exceeded; truncated history and retrying"
);
continue; // Retry with truncated messages (counts as an attempt)
}
}
let non_retryable_rate_limit = is_non_retryable_rate_limit(&e);
let non_retryable = is_non_retryable(&e) || non_retryable_rate_limit;
let rate_limited = is_rate_limited(&e);
@@ -494,14 +556,6 @@ impl Provider for ReliableProvider {
error = %error_detail,
"Non-retryable error, moving on"
);
if is_context_window_exceeded(&e) {
anyhow::bail!(
"Request exceeds model context window; retries and fallbacks were skipped. Attempts:\n{}",
failures.join("\n")
);
}
break;
}
@@ -559,6 +613,8 @@ impl Provider for ReliableProvider {
) -> anyhow::Result<ChatResponse> {
let models = self.model_chain(model);
let mut failures = Vec::new();
let mut effective_messages = messages.to_vec();
let mut context_truncated = false;
for current_model in &models {
for (provider_name, provider) in &self.providers {
@@ -566,22 +622,39 @@ impl Provider for ReliableProvider {
for attempt in 0..=self.max_retries {
match provider
.chat_with_tools(messages, tools, current_model, temperature)
.chat_with_tools(&effective_messages, tools, current_model, temperature)
.await
{
Ok(resp) => {
if attempt > 0 || *current_model != model {
if attempt > 0 || *current_model != model || context_truncated {
tracing::info!(
provider = provider_name,
model = *current_model,
attempt,
original_model = model,
context_truncated,
"Provider recovered (failover/retry)"
);
}
return Ok(resp);
}
Err(e) => {
// Context window exceeded: truncate history and retry
if is_context_window_exceeded(&e) && !context_truncated {
let dropped = truncate_for_context(&mut effective_messages);
if dropped > 0 {
context_truncated = true;
tracing::warn!(
provider = provider_name,
model = *current_model,
dropped,
remaining = effective_messages.len(),
"Context window exceeded; truncated history and retrying"
);
continue; // Retry with truncated messages (counts as an attempt)
}
}
let non_retryable_rate_limit = is_non_retryable_rate_limit(&e);
let non_retryable = is_non_retryable(&e) || non_retryable_rate_limit;
let rate_limited = is_rate_limited(&e);
@@ -618,14 +691,6 @@ impl Provider for ReliableProvider {
error = %error_detail,
"Non-retryable error, moving on"
);
if is_context_window_exceeded(&e) {
anyhow::bail!(
"Request exceeds model context window; retries and fallbacks were skipped. Attempts:\n{}",
failures.join("\n")
);
}
break;
}
@@ -669,6 +734,8 @@ impl Provider for ReliableProvider {
) -> anyhow::Result<ChatResponse> {
let models = self.model_chain(model);
let mut failures = Vec::new();
let mut effective_messages = request.messages.to_vec();
let mut context_truncated = false;
for current_model in &models {
for (provider_name, provider) in &self.providers {
@@ -676,23 +743,40 @@ impl Provider for ReliableProvider {
for attempt in 0..=self.max_retries {
let req = ChatRequest {
messages: request.messages,
messages: &effective_messages,
tools: request.tools,
};
match provider.chat(req, current_model, temperature).await {
Ok(resp) => {
if attempt > 0 || *current_model != model {
if attempt > 0 || *current_model != model || context_truncated {
tracing::info!(
provider = provider_name,
model = *current_model,
attempt,
original_model = model,
context_truncated,
"Provider recovered (failover/retry)"
);
}
return Ok(resp);
}
Err(e) => {
// Context window exceeded: truncate history and retry
if is_context_window_exceeded(&e) && !context_truncated {
let dropped = truncate_for_context(&mut effective_messages);
if dropped > 0 {
context_truncated = true;
tracing::warn!(
provider = provider_name,
model = *current_model,
dropped,
remaining = effective_messages.len(),
"Context window exceeded; truncated history and retrying"
);
continue; // Retry with truncated messages (counts as an attempt)
}
}
let non_retryable_rate_limit = is_non_retryable_rate_limit(&e);
let non_retryable = is_non_retryable(&e) || non_retryable_rate_limit;
let rate_limited = is_rate_limited(&e);
@@ -729,14 +813,6 @@ impl Provider for ReliableProvider {
error = %error_detail,
"Non-retryable error, moving on"
);
if is_context_window_exceeded(&e) {
anyhow::bail!(
"Request exceeds model context window; retries and fallbacks were skipped. Attempts:\n{}",
failures.join("\n")
);
}
break;
}
@@ -1071,7 +1147,8 @@ mod tests {
assert!(!is_non_retryable(&anyhow::anyhow!(
"model overloaded, try again later"
)));
assert!(is_non_retryable(&anyhow::anyhow!(
// Context window errors are now recoverable (not non-retryable)
assert!(!is_non_retryable(&anyhow::anyhow!(
"OpenAI Codex stream error: Your input exceeds the context window of this model."
)));
}
@@ -1107,7 +1184,7 @@ mod tests {
let msg = err.to_string();
assert!(msg.contains("context window"));
assert!(msg.contains("skipped"));
// chat_with_system has no history to truncate, so it bails immediately
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
@@ -1980,4 +2057,136 @@ mod tests {
assert_eq!(primary_calls.load(Ordering::SeqCst), 1);
assert_eq!(fallback_calls.load(Ordering::SeqCst), 1);
}
// ── Context window truncation tests ─────────────────────────
#[test]
fn context_window_error_is_not_non_retryable() {
// Context window errors should be recoverable via truncation
assert!(!is_non_retryable(&anyhow::anyhow!(
"exceeds the context window"
)));
assert!(!is_non_retryable(&anyhow::anyhow!(
"maximum context length exceeded"
)));
assert!(!is_non_retryable(&anyhow::anyhow!(
"too many tokens in the request"
)));
assert!(!is_non_retryable(&anyhow::anyhow!("token limit exceeded")));
}
#[test]
fn is_context_window_exceeded_detects_llamacpp() {
assert!(is_context_window_exceeded(&anyhow::anyhow!(
"request (8968 tokens) exceeds the available context size (8448 tokens), try increasing it"
)));
}
#[test]
fn truncate_for_context_drops_oldest_non_system() {
let mut messages = vec![
ChatMessage::system("sys"),
ChatMessage::user("msg1"),
ChatMessage::assistant("resp1"),
ChatMessage::user("msg2"),
ChatMessage::assistant("resp2"),
ChatMessage::user("msg3"),
];
let dropped = truncate_for_context(&mut messages);
// 5 non-system messages, drop oldest half = 2
assert_eq!(dropped, 2);
// System message preserved
assert_eq!(messages[0].role, "system");
// Remaining messages should be the newer ones
assert_eq!(messages.len(), 4); // system + 3 remaining non-system
// The last message should still be the most recent user message
assert_eq!(messages.last().unwrap().content, "msg3");
}
#[test]
fn truncate_for_context_preserves_system_and_last_message() {
// Only one non-system message: nothing to drop
let mut messages = vec![ChatMessage::system("sys"), ChatMessage::user("only")];
let dropped = truncate_for_context(&mut messages);
assert_eq!(dropped, 0);
assert_eq!(messages.len(), 2);
// No system message, only one user message
let mut messages = vec![ChatMessage::user("only")];
let dropped = truncate_for_context(&mut messages);
assert_eq!(dropped, 0);
assert_eq!(messages.len(), 1);
}
/// Mock that fails with context error on first N calls, then succeeds.
/// Tracks the number of messages received on each call.
struct ContextOverflowMock {
calls: Arc<AtomicUsize>,
fail_until_attempt: usize,
message_counts: parking_lot::Mutex<Vec<usize>>,
}
#[async_trait]
impl Provider for ContextOverflowMock {
async fn chat_with_system(
&self,
_system_prompt: Option<&str>,
_message: &str,
_model: &str,
_temperature: f64,
) -> anyhow::Result<String> {
Ok("ok".to_string())
}
async fn chat_with_history(
&self,
messages: &[ChatMessage],
_model: &str,
_temperature: f64,
) -> anyhow::Result<String> {
let attempt = self.calls.fetch_add(1, Ordering::SeqCst) + 1;
self.message_counts.lock().push(messages.len());
if attempt <= self.fail_until_attempt {
anyhow::bail!(
"request (8968 tokens) exceeds the available context size (8448 tokens), try increasing it"
);
}
Ok("recovered after truncation".to_string())
}
}
#[tokio::test]
async fn chat_with_history_truncates_on_context_overflow() {
let calls = Arc::new(AtomicUsize::new(0));
let mock = ContextOverflowMock {
calls: Arc::clone(&calls),
fail_until_attempt: 1, // fail first call, succeed after truncation
message_counts: parking_lot::Mutex::new(Vec::new()),
};
let provider = ReliableProvider::new(
vec![("local".into(), Box::new(mock) as Box<dyn Provider>)],
3,
1,
);
let messages = vec![
ChatMessage::system("system prompt"),
ChatMessage::user("old message 1"),
ChatMessage::assistant("old response 1"),
ChatMessage::user("old message 2"),
ChatMessage::assistant("old response 2"),
ChatMessage::user("current question"),
];
let result = provider
.chat_with_history(&messages, "local-model", 0.0)
.await
.unwrap();
assert_eq!(result, "recovered after truncation");
// Should have been called twice: once with full messages, once with truncated
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
}
+897
View File
@@ -0,0 +1,897 @@
// Autonomous skill creation from successful multi-step task executions.
//
// After the agent completes a multi-step tool-call sequence, this module
// can persist the execution as a reusable skill definition (SKILL.toml)
// under `~/.zeroclaw/workspace/skills/<slug>/`.
use crate::config::SkillCreationConfig;
use crate::memory::embeddings::EmbeddingProvider;
use crate::memory::vector::cosine_similarity;
use anyhow::{Context, Result};
use std::path::PathBuf;
/// A record of a single tool call executed during a task.
#[derive(Debug, Clone)]
pub struct ToolCallRecord {
pub name: String,
pub args: serde_json::Value,
}
/// Creates reusable skill definitions from successful multi-step executions.
pub struct SkillCreator {
workspace_dir: PathBuf,
config: SkillCreationConfig,
}
impl SkillCreator {
pub fn new(workspace_dir: PathBuf, config: SkillCreationConfig) -> Self {
Self {
workspace_dir,
config,
}
}
/// Attempt to create a skill from a successful multi-step task execution.
/// Returns `Ok(Some(slug))` if a skill was created, `Ok(None)` if skipped
/// (disabled, duplicate, or insufficient tool calls).
pub async fn create_from_execution(
&self,
task_description: &str,
tool_calls: &[ToolCallRecord],
embedding_provider: Option<&dyn EmbeddingProvider>,
) -> Result<Option<String>> {
if !self.config.enabled {
return Ok(None);
}
if tool_calls.len() < 2 {
return Ok(None);
}
// Deduplicate via embeddings when an embedding provider is available.
if let Some(provider) = embedding_provider {
if provider.name() != "none" && self.is_duplicate(task_description, provider).await? {
return Ok(None);
}
}
let slug = Self::generate_slug(task_description);
if !Self::validate_slug(&slug) {
return Ok(None);
}
// Enforce LRU limit before writing a new skill.
self.enforce_lru_limit().await?;
let skill_dir = self.skills_dir().join(&slug);
tokio::fs::create_dir_all(&skill_dir)
.await
.with_context(|| {
format!("Failed to create skill directory: {}", skill_dir.display())
})?;
let toml_content = Self::generate_skill_toml(&slug, task_description, tool_calls);
let toml_path = skill_dir.join("SKILL.toml");
tokio::fs::write(&toml_path, toml_content.as_bytes())
.await
.with_context(|| format!("Failed to write {}", toml_path.display()))?;
Ok(Some(slug))
}
/// Generate a URL-safe slug from a task description.
/// Alphanumeric and hyphens only, max 64 characters.
fn generate_slug(description: &str) -> String {
let slug: String = description
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();
// Collapse consecutive hyphens.
let mut collapsed = String::with_capacity(slug.len());
let mut prev_hyphen = false;
for c in slug.chars() {
if c == '-' {
if !prev_hyphen {
collapsed.push('-');
}
prev_hyphen = true;
} else {
collapsed.push(c);
prev_hyphen = false;
}
}
// Trim leading/trailing hyphens, then truncate.
let trimmed = collapsed.trim_matches('-');
if trimmed.len() > 64 {
// Truncate at a hyphen boundary if possible.
let truncated = &trimmed[..64];
truncated.trim_end_matches('-').to_string()
} else {
trimmed.to_string()
}
}
/// Validate that a slug is non-empty, alphanumeric + hyphens, max 64 chars.
fn validate_slug(slug: &str) -> bool {
!slug.is_empty()
&& slug.len() <= 64
&& slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
&& !slug.starts_with('-')
&& !slug.ends_with('-')
}
/// Generate SKILL.toml content from task execution data.
fn generate_skill_toml(slug: &str, description: &str, tool_calls: &[ToolCallRecord]) -> String {
use std::fmt::Write;
let mut toml = String::new();
toml.push_str("[skill]\n");
let _ = writeln!(toml, "name = {}", toml_escape(slug));
let _ = writeln!(
toml,
"description = {}",
toml_escape(&format!("Auto-generated: {description}"))
);
toml.push_str("version = \"0.1.0\"\n");
toml.push_str("author = \"zeroclaw-auto\"\n");
toml.push_str("tags = [\"auto-generated\"]\n");
for call in tool_calls {
toml.push('\n');
toml.push_str("[[tools]]\n");
let _ = writeln!(toml, "name = {}", toml_escape(&call.name));
let _ = writeln!(
toml,
"description = {}",
toml_escape(&format!("Tool used in task: {}", call.name))
);
toml.push_str("kind = \"shell\"\n");
// Extract the command from args if available, otherwise use the tool name.
let command = call
.args
.get("command")
.and_then(serde_json::Value::as_str)
.unwrap_or(&call.name);
let _ = writeln!(toml, "command = {}", toml_escape(command));
}
toml
}
/// Check if a skill with a similar description already exists.
async fn is_duplicate(
&self,
description: &str,
embedding_provider: &dyn EmbeddingProvider,
) -> Result<bool> {
let new_embedding = embedding_provider.embed_one(description).await?;
if new_embedding.is_empty() {
return Ok(false);
}
let skills_dir = self.skills_dir();
if !skills_dir.exists() {
return Ok(false);
}
let mut entries = tokio::fs::read_dir(&skills_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let toml_path = entry.path().join("SKILL.toml");
if !toml_path.exists() {
continue;
}
let content = tokio::fs::read_to_string(&toml_path).await?;
// Extract description from the TOML to compare.
if let Some(desc) = extract_description_from_toml(&content) {
let existing_embedding = embedding_provider.embed_one(&desc).await?;
if !existing_embedding.is_empty() {
#[allow(clippy::cast_possible_truncation)]
let similarity =
f64::from(cosine_similarity(&new_embedding, &existing_embedding));
if similarity > self.config.similarity_threshold {
return Ok(true);
}
}
}
}
Ok(false)
}
/// Remove the oldest auto-generated skill when we exceed `max_skills`.
async fn enforce_lru_limit(&self) -> Result<()> {
let skills_dir = self.skills_dir();
if !skills_dir.exists() {
return Ok(());
}
let mut auto_skills: Vec<(PathBuf, std::time::SystemTime)> = Vec::new();
let mut entries = tokio::fs::read_dir(&skills_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let toml_path = entry.path().join("SKILL.toml");
if !toml_path.exists() {
continue;
}
let content = tokio::fs::read_to_string(&toml_path).await?;
if content.contains("\"zeroclaw-auto\"") || content.contains("\"auto-generated\"") {
let modified = tokio::fs::metadata(&toml_path)
.await?
.modified()
.unwrap_or(std::time::UNIX_EPOCH);
auto_skills.push((entry.path(), modified));
}
}
// If at or above the limit, remove the oldest.
if auto_skills.len() >= self.config.max_skills {
auto_skills.sort_by_key(|(_, modified)| *modified);
if let Some((oldest_dir, _)) = auto_skills.first() {
tokio::fs::remove_dir_all(oldest_dir)
.await
.with_context(|| {
format!(
"Failed to remove oldest auto-generated skill: {}",
oldest_dir.display()
)
})?;
}
}
Ok(())
}
fn skills_dir(&self) -> PathBuf {
self.workspace_dir.join("skills")
}
}
/// Escape a string for TOML value (double-quoted).
fn toml_escape(s: &str) -> String {
let escaped = s
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
format!("\"{escaped}\"")
}
/// Extract the description field from a SKILL.toml string.
fn extract_description_from_toml(content: &str) -> Option<String> {
#[derive(serde::Deserialize)]
struct Partial {
skill: PartialSkill,
}
#[derive(serde::Deserialize)]
struct PartialSkill {
description: Option<String>,
}
toml::from_str::<Partial>(content)
.ok()
.and_then(|p| p.skill.description)
}
/// Extract `ToolCallRecord`s from the agent conversation history.
///
/// Scans assistant messages for tool call patterns (both JSON and XML formats)
/// and returns records for each unique tool invocation.
pub fn extract_tool_calls_from_history(
history: &[crate::providers::ChatMessage],
) -> Vec<ToolCallRecord> {
let mut records = Vec::new();
for msg in history {
if msg.role != "assistant" {
continue;
}
// Try parsing as JSON (native tool_calls format).
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&msg.content) {
if let Some(tool_calls) = value.get("tool_calls").and_then(|v| v.as_array()) {
for call in tool_calls {
if let Some(function) = call.get("function") {
let name = function
.get("name")
.and_then(serde_json::Value::as_str)
.unwrap_or("")
.to_string();
let args_str = function
.get("arguments")
.and_then(serde_json::Value::as_str)
.unwrap_or("{}");
let args = serde_json::from_str(args_str).unwrap_or_default();
if !name.is_empty() {
records.push(ToolCallRecord { name, args });
}
}
}
}
}
// Also try XML tool call format: <tool_name>...</tool_name>
// Simple extraction for `<shell>{"command":"..."}</shell>` style tags.
let content = &msg.content;
let mut pos = 0;
while pos < content.len() {
if let Some(start) = content[pos..].find('<') {
let abs_start = pos + start;
if let Some(end) = content[abs_start..].find('>') {
let tag = &content[abs_start + 1..abs_start + end];
// Skip closing tags and meta tags.
if tag.starts_with('/') || tag.starts_with('!') || tag.starts_with('?') {
pos = abs_start + end + 1;
continue;
}
let tag_name = tag.split_whitespace().next().unwrap_or(tag);
let close_tag = format!("</{tag_name}>");
if let Some(close_pos) = content[abs_start + end + 1..].find(&close_tag) {
let inner = &content[abs_start + end + 1..abs_start + end + 1 + close_pos];
let args: serde_json::Value =
serde_json::from_str(inner.trim()).unwrap_or_default();
// Only add if it looks like a tool call (not HTML/formatting tags).
if tag_name != "tool_result"
&& tag_name != "tool_results"
&& !tag_name.contains(':')
&& args.is_object()
&& !args.as_object().map_or(true, |o| o.is_empty())
{
records.push(ToolCallRecord {
name: tag_name.to_string(),
args,
});
}
pos = abs_start + end + 1 + close_pos + close_tag.len();
} else {
pos = abs_start + end + 1;
}
} else {
break;
}
} else {
break;
}
}
}
records
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::embeddings::{EmbeddingProvider, NoopEmbedding};
use async_trait::async_trait;
// ── Slug generation ──────────────────────────────────────────
#[test]
fn slug_basic() {
assert_eq!(
SkillCreator::generate_slug("Deploy to production"),
"deploy-to-production"
);
}
#[test]
fn slug_special_characters() {
assert_eq!(
SkillCreator::generate_slug("Build & test (CI/CD) pipeline!"),
"build-test-ci-cd-pipeline"
);
}
#[test]
fn slug_max_length() {
let long_desc = "a".repeat(100);
let slug = SkillCreator::generate_slug(&long_desc);
assert!(slug.len() <= 64);
}
#[test]
fn slug_leading_trailing_hyphens() {
let slug = SkillCreator::generate_slug("---hello world---");
assert!(!slug.starts_with('-'));
assert!(!slug.ends_with('-'));
}
#[test]
fn slug_consecutive_spaces() {
assert_eq!(SkillCreator::generate_slug("hello world"), "hello-world");
}
#[test]
fn slug_empty_input() {
let slug = SkillCreator::generate_slug("");
assert!(slug.is_empty());
}
#[test]
fn slug_only_symbols() {
let slug = SkillCreator::generate_slug("!@#$%^&*()");
assert!(slug.is_empty());
}
#[test]
fn slug_unicode() {
let slug = SkillCreator::generate_slug("Deploy cafe app");
assert_eq!(slug, "deploy-cafe-app");
}
// ── Slug validation ──────────────────────────────────────────
#[test]
fn validate_slug_valid() {
assert!(SkillCreator::validate_slug("deploy-to-production"));
assert!(SkillCreator::validate_slug("a"));
assert!(SkillCreator::validate_slug("abc123"));
}
#[test]
fn validate_slug_invalid() {
assert!(!SkillCreator::validate_slug(""));
assert!(!SkillCreator::validate_slug("-starts-with-hyphen"));
assert!(!SkillCreator::validate_slug("ends-with-hyphen-"));
assert!(!SkillCreator::validate_slug("has spaces"));
assert!(!SkillCreator::validate_slug("has_underscores"));
assert!(!SkillCreator::validate_slug(&"a".repeat(65)));
}
// ── TOML generation ──────────────────────────────────────────
#[test]
fn toml_generation_valid_format() {
let calls = vec![
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "cargo build"}),
},
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "cargo test"}),
},
];
let toml_str = SkillCreator::generate_skill_toml(
"build-and-test",
"Build and test the project",
&calls,
);
// Should parse as valid TOML.
let parsed: toml::Value =
toml::from_str(&toml_str).expect("Generated TOML should be valid");
let skill = parsed.get("skill").expect("Should have [skill] section");
assert_eq!(
skill.get("name").and_then(toml::Value::as_str),
Some("build-and-test")
);
assert_eq!(
skill.get("author").and_then(toml::Value::as_str),
Some("zeroclaw-auto")
);
assert_eq!(
skill.get("version").and_then(toml::Value::as_str),
Some("0.1.0")
);
let tools = parsed.get("tools").and_then(toml::Value::as_array).unwrap();
assert_eq!(tools.len(), 2);
assert_eq!(
tools[0].get("command").and_then(toml::Value::as_str),
Some("cargo build")
);
}
#[test]
fn toml_generation_escapes_quotes() {
let calls = vec![ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "echo \"hello\""}),
}];
let toml_str =
SkillCreator::generate_skill_toml("echo-test", "Test \"quoted\" description", &calls);
let parsed: toml::Value =
toml::from_str(&toml_str).expect("TOML with quotes should be valid");
let desc = parsed
.get("skill")
.and_then(|s| s.get("description"))
.and_then(toml::Value::as_str)
.unwrap();
assert!(desc.contains("quoted"));
}
#[test]
fn toml_generation_no_command_arg() {
let calls = vec![ToolCallRecord {
name: "memory_store".into(),
args: serde_json::json!({"key": "foo", "value": "bar"}),
}];
let toml_str = SkillCreator::generate_skill_toml("memory-op", "Store to memory", &calls);
let parsed: toml::Value = toml::from_str(&toml_str).expect("TOML should be valid");
let tools = parsed.get("tools").and_then(toml::Value::as_array).unwrap();
// When no "command" arg exists, falls back to tool name.
assert_eq!(
tools[0].get("command").and_then(toml::Value::as_str),
Some("memory_store")
);
}
// ── TOML description extraction ──────────────────────────────
#[test]
fn extract_description_from_valid_toml() {
let content = r#"
[skill]
name = "test"
description = "Auto-generated: Build project"
version = "0.1.0"
"#;
assert_eq!(
extract_description_from_toml(content),
Some("Auto-generated: Build project".into())
);
}
#[test]
fn extract_description_from_invalid_toml() {
assert_eq!(extract_description_from_toml("not valid toml {{"), None);
}
// ── Deduplication ────────────────────────────────────────────
/// A mock embedding provider that returns deterministic embeddings.
///
/// The "new" description (first text embedded) always gets `[1, 0, 0]`.
/// The "existing" skill description (second text embedded) gets a vector
/// whose cosine similarity with `[1, 0, 0]` equals `self.similarity`.
struct MockEmbeddingProvider {
similarity: f32,
call_count: std::sync::atomic::AtomicUsize,
}
impl MockEmbeddingProvider {
fn new(similarity: f32) -> Self {
Self {
similarity,
call_count: std::sync::atomic::AtomicUsize::new(0),
}
}
}
#[async_trait]
impl EmbeddingProvider for MockEmbeddingProvider {
fn name(&self) -> &str {
"mock"
}
fn dimensions(&self) -> usize {
3
}
async fn embed(&self, texts: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
Ok(texts
.iter()
.map(|_| {
let call = self
.call_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if call == 0 {
// First call: the "new" description.
vec![1.0, 0.0, 0.0]
} else {
// Subsequent calls: existing skill descriptions.
// Produce a vector with the configured cosine similarity to [1,0,0].
vec![
self.similarity,
(1.0 - self.similarity * self.similarity).sqrt(),
0.0,
]
}
})
.collect())
}
}
#[tokio::test]
async fn dedup_skips_similar_descriptions() {
let dir = tempfile::tempdir().unwrap();
let skills_dir = dir.path().join("skills").join("existing-skill");
tokio::fs::create_dir_all(&skills_dir).await.unwrap();
tokio::fs::write(
skills_dir.join("SKILL.toml"),
r#"
[skill]
name = "existing-skill"
description = "Auto-generated: Build the project"
version = "0.1.0"
author = "zeroclaw-auto"
tags = ["auto-generated"]
"#,
)
.await
.unwrap();
let config = SkillCreationConfig {
enabled: true,
max_skills: 500,
similarity_threshold: 0.85,
};
// High similarity provider -> should detect as duplicate.
let provider = MockEmbeddingProvider::new(0.95);
let creator = SkillCreator::new(dir.path().to_path_buf(), config.clone());
assert!(creator
.is_duplicate("Build the project", &provider)
.await
.unwrap());
// Low similarity provider -> not a duplicate.
let provider_low = MockEmbeddingProvider::new(0.3);
let creator2 = SkillCreator::new(dir.path().to_path_buf(), config);
assert!(!creator2
.is_duplicate("Completely different task", &provider_low)
.await
.unwrap());
}
// ── LRU eviction ─────────────────────────────────────────────
#[tokio::test]
async fn lru_eviction_removes_oldest() {
let dir = tempfile::tempdir().unwrap();
let config = SkillCreationConfig {
enabled: true,
max_skills: 2,
similarity_threshold: 0.85,
};
let skills_dir = dir.path().join("skills");
// Create two auto-generated skills with different timestamps.
for (i, name) in ["old-skill", "new-skill"].iter().enumerate() {
let skill_dir = skills_dir.join(name);
tokio::fs::create_dir_all(&skill_dir).await.unwrap();
tokio::fs::write(
skill_dir.join("SKILL.toml"),
format!(
r#"[skill]
name = "{name}"
description = "Auto-generated: Skill {i}"
version = "0.1.0"
author = "zeroclaw-auto"
tags = ["auto-generated"]
"#
),
)
.await
.unwrap();
// Small delay to ensure different timestamps.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
let creator = SkillCreator::new(dir.path().to_path_buf(), config);
creator.enforce_lru_limit().await.unwrap();
// The oldest skill should have been removed.
assert!(!skills_dir.join("old-skill").exists());
assert!(skills_dir.join("new-skill").exists());
}
// ── End-to-end: create_from_execution ────────────────────────
#[tokio::test]
async fn create_from_execution_disabled() {
let dir = tempfile::tempdir().unwrap();
let config = SkillCreationConfig {
enabled: false,
..Default::default()
};
let creator = SkillCreator::new(dir.path().to_path_buf(), config);
let calls = vec![
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "ls"}),
},
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "pwd"}),
},
];
let result = creator
.create_from_execution("List files", &calls, None)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn create_from_execution_insufficient_steps() {
let dir = tempfile::tempdir().unwrap();
let config = SkillCreationConfig {
enabled: true,
..Default::default()
};
let creator = SkillCreator::new(dir.path().to_path_buf(), config);
let calls = vec![ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "ls"}),
}];
let result = creator
.create_from_execution("List files", &calls, None)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn create_from_execution_success() {
let dir = tempfile::tempdir().unwrap();
let config = SkillCreationConfig {
enabled: true,
max_skills: 500,
similarity_threshold: 0.85,
};
let creator = SkillCreator::new(dir.path().to_path_buf(), config);
let calls = vec![
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "cargo build"}),
},
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "cargo test"}),
},
];
// Use noop embedding (no deduplication).
let noop = NoopEmbedding;
let result = creator
.create_from_execution("Build and test", &calls, Some(&noop))
.await
.unwrap();
assert_eq!(result, Some("build-and-test".into()));
// Verify the skill directory and TOML were created.
let skill_dir = dir.path().join("skills").join("build-and-test");
assert!(skill_dir.exists());
let toml_content = tokio::fs::read_to_string(skill_dir.join("SKILL.toml"))
.await
.unwrap();
assert!(toml_content.contains("build-and-test"));
assert!(toml_content.contains("zeroclaw-auto"));
}
#[tokio::test]
async fn create_from_execution_with_dedup() {
let dir = tempfile::tempdir().unwrap();
let config = SkillCreationConfig {
enabled: true,
max_skills: 500,
similarity_threshold: 0.85,
};
// First, create an existing skill.
let skills_dir = dir.path().join("skills").join("existing");
tokio::fs::create_dir_all(&skills_dir).await.unwrap();
tokio::fs::write(
skills_dir.join("SKILL.toml"),
r#"[skill]
name = "existing"
description = "Auto-generated: Build and test"
version = "0.1.0"
author = "zeroclaw-auto"
tags = ["auto-generated"]
"#,
)
.await
.unwrap();
// High similarity provider -> should skip.
let provider = MockEmbeddingProvider::new(0.95);
let creator = SkillCreator::new(dir.path().to_path_buf(), config);
let calls = vec![
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "cargo build"}),
},
ToolCallRecord {
name: "shell".into(),
args: serde_json::json!({"command": "cargo test"}),
},
];
let result = creator
.create_from_execution("Build and test", &calls, Some(&provider))
.await
.unwrap();
assert!(result.is_none());
}
// ── Tool call extraction from history ────────────────────────
#[test]
fn extract_from_empty_history() {
let history = vec![];
let records = extract_tool_calls_from_history(&history);
assert!(records.is_empty());
}
#[test]
fn extract_from_user_messages_only() {
use crate::providers::ChatMessage;
let history = vec![ChatMessage::user("hello"), ChatMessage::user("world")];
let records = extract_tool_calls_from_history(&history);
assert!(records.is_empty());
}
// ── Fuzz-like tests for slug ─────────────────────────────────
#[test]
fn slug_fuzz_various_inputs() {
let inputs = [
"",
" ",
"---",
"a",
"hello world!",
"UPPER CASE",
"with-hyphens-already",
"with__underscores",
"123 numbers 456",
"emoji: cafe",
&"x".repeat(200),
"a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-0-1-2-3-4-5",
];
for input in &inputs {
let slug = SkillCreator::generate_slug(input);
// Slug should always pass validation (or be empty for degenerate input).
if !slug.is_empty() {
assert!(
SkillCreator::validate_slug(&slug),
"Generated slug '{slug}' from '{input}' failed validation"
);
}
}
}
// ── Fuzz-like tests for TOML generation ──────────────────────
#[test]
fn toml_fuzz_various_inputs() {
let descriptions = [
"simple task",
"task with \"quotes\" and \\ backslashes",
"task with\nnewlines\r\nand tabs\there",
"",
&"long ".repeat(100),
];
let args_variants = [
serde_json::json!({}),
serde_json::json!({"command": "echo hello"}),
serde_json::json!({"command": "echo \"hello world\"", "extra": 42}),
];
for desc in &descriptions {
for args in &args_variants {
let calls = vec![
ToolCallRecord {
name: "tool1".into(),
args: args.clone(),
},
ToolCallRecord {
name: "tool2".into(),
args: args.clone(),
},
];
let toml_str = SkillCreator::generate_skill_toml("test-slug", desc, &calls);
// Must always produce valid TOML.
let _parsed: toml::Value = toml::from_str(&toml_str)
.unwrap_or_else(|e| panic!("Invalid TOML for desc '{desc}': {e}\n{toml_str}"));
}
}
}
}
+2
View File
@@ -7,6 +7,8 @@ use std::process::Command;
use std::time::{Duration, SystemTime};
mod audit;
#[cfg(feature = "skill-creation")]
pub mod creator;
const OPEN_SKILLS_REPO_URL: &str = "https://github.com/besoeasy/open-skills";
const OPEN_SKILLS_SYNC_MARKER: &str = ".zeroclaw-open-skills-sync";
+61 -2
View File
@@ -1,6 +1,8 @@
use super::traits::{Tool, ToolResult};
use crate::config::Config;
use crate::cron::{self, DeliveryConfig, JobType, Schedule, SessionTarget};
use crate::cron::{
self, deserialize_maybe_stringified, DeliveryConfig, JobType, Schedule, SessionTarget,
};
use crate::security::SecurityPolicy;
use async_trait::async_trait;
use serde_json::json;
@@ -176,7 +178,7 @@ impl Tool for CronAddTool {
}
let schedule = match args.get("schedule") {
Some(v) => match serde_json::from_value::<Schedule>(v.clone()) {
Some(v) => match deserialize_maybe_stringified::<Schedule>(v) {
Ok(schedule) => schedule,
Err(e) => {
return Ok(ToolResult {
@@ -511,6 +513,63 @@ mod tests {
assert!(approved.success, "{:?}", approved.error);
}
#[tokio::test]
async fn accepts_schedule_passed_as_json_string() {
let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp).await;
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
// Simulate the LLM double-serializing the schedule: the value arrives
// as a JSON string containing a JSON object, rather than an object.
let result = tool
.execute(json!({
"schedule": r#"{"kind":"cron","expr":"*/5 * * * *"}"#,
"job_type": "shell",
"command": "echo string-schedule"
}))
.await
.unwrap();
assert!(result.success, "{:?}", result.error);
assert!(result.output.contains("next_run"));
}
#[tokio::test]
async fn accepts_stringified_interval_schedule() {
let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp).await;
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
let result = tool
.execute(json!({
"schedule": r#"{"kind":"every","every_ms":60000}"#,
"job_type": "shell",
"command": "echo interval"
}))
.await
.unwrap();
assert!(result.success, "{:?}", result.error);
}
#[tokio::test]
async fn accepts_stringified_schedule_with_timezone() {
let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp).await;
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
let result = tool
.execute(json!({
"schedule": r#"{"kind":"cron","expr":"*/30 9-15 * * 1-5","tz":"Asia/Shanghai"}"#,
"job_type": "shell",
"command": "echo tz-test"
}))
.await
.unwrap();
assert!(result.success, "{:?}", result.error);
}
#[tokio::test]
async fn rejects_invalid_schedule() {
let tmp = TempDir::new().unwrap();
+2 -2
View File
@@ -1,6 +1,6 @@
use super::traits::{Tool, ToolResult};
use crate::config::Config;
use crate::cron::{self, CronJobPatch};
use crate::cron::{self, deserialize_maybe_stringified, CronJobPatch};
use crate::security::SecurityPolicy;
use async_trait::async_trait;
use serde_json::json;
@@ -202,7 +202,7 @@ impl Tool for CronUpdateTool {
}
};
let patch = match serde_json::from_value::<CronJobPatch>(patch_val) {
let patch = match deserialize_maybe_stringified::<CronJobPatch>(&patch_val) {
Ok(patch) => patch,
Err(e) => {
return Ok(ToolResult {
+243 -4
View File
@@ -296,8 +296,9 @@ impl Tool for DelegateTool {
}
// Wrap the provider call in a timeout to prevent indefinite blocking
let timeout_secs = agent_config.timeout_secs.unwrap_or(DELEGATE_TIMEOUT_SECS);
let result = tokio::time::timeout(
Duration::from_secs(DELEGATE_TIMEOUT_SECS),
Duration::from_secs(timeout_secs),
provider.chat_with_system(
agent_config.system_prompt.as_deref(),
&full_prompt,
@@ -314,7 +315,7 @@ impl Tool for DelegateTool {
success: false,
output: String::new(),
error: Some(format!(
"Agent '{agent_name}' timed out after {DELEGATE_TIMEOUT_SECS}s"
"Agent '{agent_name}' timed out after {timeout_secs}s"
)),
});
}
@@ -401,8 +402,11 @@ impl DelegateTool {
let noop_observer = NoopObserver;
let agentic_timeout_secs = agent_config
.agentic_timeout_secs
.unwrap_or(DELEGATE_AGENTIC_TIMEOUT_SECS);
let result = tokio::time::timeout(
Duration::from_secs(DELEGATE_AGENTIC_TIMEOUT_SECS),
Duration::from_secs(agentic_timeout_secs),
run_tool_call_loop(
provider,
&mut history,
@@ -422,6 +426,7 @@ impl DelegateTool {
&[],
&[],
None,
None,
),
)
.await;
@@ -453,7 +458,7 @@ impl DelegateTool {
success: false,
output: String::new(),
error: Some(format!(
"Agent '{agent_name}' timed out after {DELEGATE_AGENTIC_TIMEOUT_SECS}s"
"Agent '{agent_name}' timed out after {agentic_timeout_secs}s"
)),
}),
}
@@ -530,6 +535,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
agents.insert(
@@ -544,6 +551,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
agents
@@ -697,6 +706,8 @@ mod tests {
agentic: true,
allowed_tools,
max_iterations,
timeout_secs: None,
agentic_timeout_secs: None,
}
}
@@ -805,6 +816,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
let tool = DelegateTool::new(agents, None, test_security());
@@ -911,6 +924,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
let tool = DelegateTool::new(agents, None, test_security());
@@ -946,6 +961,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
let tool = DelegateTool::new(agents, None, test_security());
@@ -1220,4 +1237,226 @@ mod tests {
handle.write().push(Arc::new(FakeMcpTool));
assert_eq!(handle.read().len(), 2);
}
// ── Configurable timeout tests ──────────────────────────────────
#[test]
fn default_timeout_values_used_when_config_unset() {
let config = DelegateAgentConfig {
provider: "ollama".to_string(),
model: "llama3".to_string(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
};
assert_eq!(config.timeout_secs.unwrap_or(DELEGATE_TIMEOUT_SECS), 120);
assert_eq!(
config
.agentic_timeout_secs
.unwrap_or(DELEGATE_AGENTIC_TIMEOUT_SECS),
300
);
}
#[test]
fn custom_timeout_values_are_respected() {
let config = DelegateAgentConfig {
provider: "ollama".to_string(),
model: "llama3".to_string(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: Some(60),
agentic_timeout_secs: Some(600),
};
assert_eq!(config.timeout_secs.unwrap_or(DELEGATE_TIMEOUT_SECS), 60);
assert_eq!(
config
.agentic_timeout_secs
.unwrap_or(DELEGATE_AGENTIC_TIMEOUT_SECS),
600
);
}
#[test]
fn timeout_deserialization_defaults_to_none() {
let toml_str = r#"
provider = "ollama"
model = "llama3"
"#;
let config: DelegateAgentConfig = toml::from_str(toml_str).unwrap();
assert!(config.timeout_secs.is_none());
assert!(config.agentic_timeout_secs.is_none());
}
#[test]
fn timeout_deserialization_with_custom_values() {
let toml_str = r#"
provider = "ollama"
model = "llama3"
timeout_secs = 45
agentic_timeout_secs = 900
"#;
let config: DelegateAgentConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.timeout_secs, Some(45));
assert_eq!(config.agentic_timeout_secs, Some(900));
}
#[test]
fn config_validation_rejects_zero_timeout() {
let mut config = crate::config::Config::default();
config.agents.insert(
"bad".into(),
DelegateAgentConfig {
provider: "ollama".into(),
model: "llama3".into(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: Some(0),
agentic_timeout_secs: None,
},
);
let err = config.validate().unwrap_err();
assert!(
format!("{err}").contains("timeout_secs must be greater than 0"),
"unexpected error: {err}"
);
}
#[test]
fn config_validation_rejects_zero_agentic_timeout() {
let mut config = crate::config::Config::default();
config.agents.insert(
"bad".into(),
DelegateAgentConfig {
provider: "ollama".into(),
model: "llama3".into(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: Some(0),
},
);
let err = config.validate().unwrap_err();
assert!(
format!("{err}").contains("agentic_timeout_secs must be greater than 0"),
"unexpected error: {err}"
);
}
#[test]
fn config_validation_rejects_excessive_timeout() {
let mut config = crate::config::Config::default();
config.agents.insert(
"bad".into(),
DelegateAgentConfig {
provider: "ollama".into(),
model: "llama3".into(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: Some(7200),
agentic_timeout_secs: None,
},
);
let err = config.validate().unwrap_err();
assert!(
format!("{err}").contains("exceeds max 3600"),
"unexpected error: {err}"
);
}
#[test]
fn config_validation_rejects_excessive_agentic_timeout() {
let mut config = crate::config::Config::default();
config.agents.insert(
"bad".into(),
DelegateAgentConfig {
provider: "ollama".into(),
model: "llama3".into(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: Some(5000),
},
);
let err = config.validate().unwrap_err();
assert!(
format!("{err}").contains("exceeds max 3600"),
"unexpected error: {err}"
);
}
#[test]
fn config_validation_accepts_max_boundary_timeout() {
let mut config = crate::config::Config::default();
config.agents.insert(
"ok".into(),
DelegateAgentConfig {
provider: "ollama".into(),
model: "llama3".into(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: Some(3600),
agentic_timeout_secs: Some(3600),
},
);
assert!(config.validate().is_ok());
}
#[test]
fn config_validation_accepts_none_timeouts() {
let mut config = crate::config::Config::default();
config.agents.insert(
"ok".into(),
DelegateAgentConfig {
provider: "ollama".into(),
model: "llama3".into(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
assert!(config.validate().is_ok());
}
}
+92 -2
View File
@@ -233,12 +233,22 @@ impl Default for ActivatedToolSet {
/// Build the `<available-deferred-tools>` section for the system prompt.
/// Lists only tool names so the LLM knows what is available without
/// consuming context window on full schemas.
/// consuming context window on full schemas. Includes an instruction
/// block that tells the LLM to call `tool_search` to activate them.
pub fn build_deferred_tools_section(deferred: &DeferredMcpToolSet) -> String {
if deferred.is_empty() {
return String::new();
}
let mut out = String::from("<available-deferred-tools>\n");
let mut out = String::new();
out.push_str("## Deferred Tools\n\n");
out.push_str(
"The tools listed below are available but NOT yet loaded. \
To use any of them you MUST first call the `tool_search` tool \
to fetch their full schemas. Use `\"select:name1,name2\"` for \
exact tools or keywords to search. Once activated, the tools \
become callable for the rest of the conversation.\n\n",
);
out.push_str("<available-deferred-tools>\n");
for stub in &deferred.stubs {
out.push_str(&stub.prefixed_name);
out.push('\n');
@@ -416,6 +426,55 @@ mod tests {
assert!(section.contains("</available-deferred-tools>"));
}
#[test]
fn build_deferred_section_includes_tool_search_instruction() {
let stubs = vec![make_stub("fs__read_file", "Read a file")];
let set = DeferredMcpToolSet {
stubs,
registry: std::sync::Arc::new(
tokio::runtime::Runtime::new()
.unwrap()
.block_on(McpRegistry::connect_all(&[]))
.unwrap(),
),
};
let section = build_deferred_tools_section(&set);
assert!(
section.contains("tool_search"),
"deferred section must instruct the LLM to use tool_search"
);
assert!(
section.contains("## Deferred Tools"),
"deferred section must include a heading"
);
}
#[test]
fn build_deferred_section_multiple_servers() {
let stubs = vec![
make_stub("server_a__list", "List items"),
make_stub("server_a__create", "Create item"),
make_stub("server_b__query", "Query records"),
];
let set = DeferredMcpToolSet {
stubs,
registry: std::sync::Arc::new(
tokio::runtime::Runtime::new()
.unwrap()
.block_on(McpRegistry::connect_all(&[]))
.unwrap(),
),
};
let section = build_deferred_tools_section(&set);
assert!(section.contains("server_a__list"));
assert!(section.contains("server_a__create"));
assert!(section.contains("server_b__query"));
assert!(
section.contains("tool_search"),
"section must mention tool_search for multi-server setups"
);
}
#[test]
fn keyword_search_ranks_by_hits() {
let stubs = vec![
@@ -457,4 +516,35 @@ mod tests {
assert!(set.get_by_name("a__one").is_some());
assert!(set.get_by_name("nonexistent").is_none());
}
#[test]
fn search_across_multiple_servers() {
let stubs = vec![
make_stub("server_a__read_file", "Read a file from disk"),
make_stub("server_b__read_config", "Read configuration from database"),
];
let set = DeferredMcpToolSet {
stubs,
registry: std::sync::Arc::new(
tokio::runtime::Runtime::new()
.unwrap()
.block_on(McpRegistry::connect_all(&[]))
.unwrap(),
),
};
// "read" should match stubs from both servers
let results = set.search("read", 10);
assert_eq!(results.len(), 2);
// "file" should match only server_a
let results = set.search("file", 10);
assert_eq!(results.len(), 1);
assert_eq!(results[0].prefixed_name, "server_a__read_file");
// "config database" should rank server_b highest (2 hits)
let results = set.search("config database", 10);
assert!(!results.is_empty());
assert_eq!(results[0].prefixed_name, "server_b__read_config");
}
}
+52
View File
@@ -59,6 +59,7 @@ pub mod memory_recall;
pub mod memory_store;
pub mod microsoft365;
pub mod model_routing_config;
pub mod model_switch;
pub mod node_tool;
pub mod notion_tool;
pub mod pdf_read;
@@ -119,6 +120,7 @@ pub use memory_recall::MemoryRecallTool;
pub use memory_store::MemoryStoreTool;
pub use microsoft365::Microsoft365Tool;
pub use model_routing_config::ModelRoutingConfigTool;
pub use model_switch::ModelSwitchTool;
#[allow(unused_imports)]
pub use node_tool::NodeTool;
pub use notion_tool::NotionTool;
@@ -302,6 +304,7 @@ pub fn all_tools_with_runtime(
config.clone(),
security.clone(),
)),
Arc::new(ModelSwitchTool::new(security.clone())),
Arc::new(ProxyConfigTool::new(config.clone(), security.clone())),
Arc::new(GitOperationsTool::new(
security.clone(),
@@ -634,6 +637,53 @@ pub fn all_tools_with_runtime(
)));
}
// ── WASM plugin tools (requires plugins-wasm feature) ──
#[cfg(feature = "plugins-wasm")]
{
let plugin_dir = config.plugins.plugins_dir.clone();
let plugin_path = if plugin_dir.starts_with("~/") {
let home = directories::UserDirs::new()
.map(|u| u.home_dir().to_path_buf())
.unwrap_or_else(|| std::path::PathBuf::from("."));
home.join(&plugin_dir[2..])
} else {
std::path::PathBuf::from(&plugin_dir)
};
if plugin_path.exists() && config.plugins.enabled {
match crate::plugins::host::PluginHost::new(
plugin_path.parent().unwrap_or(&plugin_path),
) {
Ok(host) => {
let tool_manifests = host.tool_plugins();
let count = tool_manifests.len();
for manifest in tool_manifests {
tool_arcs.push(Arc::new(crate::plugins::wasm_tool::WasmTool::new(
manifest.name.clone(),
manifest.description.clone().unwrap_or_default(),
manifest.name.clone(),
"call".to_string(),
serde_json::json!({
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Input for the plugin"
}
},
"required": ["input"]
}),
)));
}
tracing::info!("Loaded {count} WASM plugin tools");
}
Err(e) => {
tracing::warn!("Failed to load WASM plugins: {e}");
}
}
}
}
(boxed_registry_from_arcs(tool_arcs), delegate_handle)
}
@@ -867,6 +917,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
+2
View File
@@ -705,6 +705,8 @@ impl ModelRoutingConfigTool {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: DEFAULT_AGENT_MAX_ITERATIONS,
timeout_secs: None,
agentic_timeout_secs: None,
});
next_agent.provider = provider;
+264
View File
@@ -0,0 +1,264 @@
use super::traits::{Tool, ToolResult};
use crate::agent::loop_::get_model_switch_state;
use crate::providers;
use crate::security::policy::ToolOperation;
use crate::security::SecurityPolicy;
use async_trait::async_trait;
use serde_json::json;
use std::sync::Arc;
pub struct ModelSwitchTool {
security: Arc<SecurityPolicy>,
}
impl ModelSwitchTool {
pub fn new(security: Arc<SecurityPolicy>) -> Self {
Self { security }
}
}
#[async_trait]
impl Tool for ModelSwitchTool {
fn name(&self) -> &str {
"model_switch"
}
fn description(&self) -> &str {
"Switch the AI model at runtime. Use 'get' to see current model, 'list_providers' to see available providers, 'list_models' to see models for a provider, or 'set' to switch to a different model. The switch takes effect immediately for the current conversation."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["get", "set", "list_providers", "list_models"],
"description": "Action to perform: get current model, set a new model, list available providers, or list models for a provider"
},
"provider": {
"type": "string",
"description": "Provider name (e.g., 'openai', 'anthropic', 'groq', 'ollama'). Required for 'set' and 'list_models' actions."
},
"model": {
"type": "string",
"description": "Model ID (e.g., 'gpt-4o', 'claude-sonnet-4-6'). Required for 'set' action."
}
},
"required": ["action"]
})
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("get");
if let Err(error) = self
.security
.enforce_tool_operation(ToolOperation::Act, "model_switch")
{
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(error),
});
}
match action {
"get" => self.handle_get(),
"set" => self.handle_set(&args),
"list_providers" => self.handle_list_providers(),
"list_models" => self.handle_list_models(&args),
_ => Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!(
"Unknown action: {}. Valid actions: get, set, list_providers, list_models",
action
)),
}),
}
}
}
impl ModelSwitchTool {
fn handle_get(&self) -> anyhow::Result<ToolResult> {
let switch_state = get_model_switch_state();
let pending = switch_state.lock().unwrap().clone();
Ok(ToolResult {
success: true,
output: serde_json::to_string_pretty(&json!({
"pending_switch": pending,
"note": "To switch models, use action 'set' with provider and model parameters"
}))?,
error: None,
})
}
fn handle_set(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
let provider = args.get("provider").and_then(|v| v.as_str());
let provider = match provider {
Some(p) => p,
None => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Missing 'provider' parameter for 'set' action".to_string()),
});
}
};
let model = args.get("model").and_then(|v| v.as_str());
let model = match model {
Some(m) => m,
None => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Missing 'model' parameter for 'set' action".to_string()),
});
}
};
// Validate the provider exists
let known_providers = providers::list_providers();
let provider_valid = known_providers.iter().any(|p| {
p.name.eq_ignore_ascii_case(provider)
|| p.aliases.iter().any(|a| a.eq_ignore_ascii_case(provider))
});
if !provider_valid {
return Ok(ToolResult {
success: false,
output: serde_json::to_string_pretty(&json!({
"available_providers": known_providers.iter().map(|p| p.name).collect::<Vec<_>>()
}))?,
error: Some(format!(
"Unknown provider: {}. Use 'list_providers' to see available options.",
provider
)),
});
}
// Set the global model switch request
let switch_state = get_model_switch_state();
*switch_state.lock().unwrap() = Some((provider.to_string(), model.to_string()));
Ok(ToolResult {
success: true,
output: serde_json::to_string_pretty(&json!({
"message": "Model switch requested",
"provider": provider,
"model": model,
"note": "The agent will switch to this model on the next turn. Use 'get' to check pending switch."
}))?,
error: None,
})
}
fn handle_list_providers(&self) -> anyhow::Result<ToolResult> {
let providers_list = providers::list_providers();
let providers: Vec<serde_json::Value> = providers_list
.iter()
.map(|p| {
json!({
"name": p.name,
"display_name": p.display_name,
"aliases": p.aliases,
"local": p.local
})
})
.collect();
Ok(ToolResult {
success: true,
output: serde_json::to_string_pretty(&json!({
"providers": providers,
"count": providers.len(),
"example": "Use action 'set' with provider and model to switch"
}))?,
error: None,
})
}
fn handle_list_models(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
let provider = args.get("provider").and_then(|v| v.as_str());
let provider = match provider {
Some(p) => p,
None => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(
"Missing 'provider' parameter for 'list_models' action".to_string(),
),
});
}
};
// Return common models for known providers
let models = match provider.to_lowercase().as_str() {
"openai" => vec![
"gpt-4o",
"gpt-4o-mini",
"gpt-4-turbo",
"gpt-4",
"gpt-3.5-turbo",
],
"anthropic" => vec![
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-3-5-sonnet",
"claude-3-opus",
"claude-3-haiku",
],
"openrouter" => vec![
"anthropic/claude-sonnet-4-6",
"openai/gpt-4o",
"google/gemini-pro",
"meta-llama/llama-3-70b-instruct",
],
"groq" => vec![
"llama-3.3-70b-versatile",
"mixtral-8x7b-32768",
"llama-3.1-70b-speculative",
],
"ollama" => vec!["llama3", "llama3.1", "mistral", "codellama", "phi3"],
"deepseek" => vec!["deepseek-chat", "deepseek-coder"],
"mistral" => vec![
"mistral-large-latest",
"mistral-small-latest",
"mistral-nemo",
],
"google" | "gemini" => vec!["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"],
"xai" | "grok" => vec!["grok-2", "grok-2-vision", "grok-beta"],
_ => vec![],
};
if models.is_empty() {
return Ok(ToolResult {
success: true,
output: serde_json::to_string_pretty(&json!({
"provider": provider,
"models": [],
"note": "No common models listed for this provider. Check provider documentation for available models."
}))?,
error: None,
});
}
Ok(ToolResult {
success: true,
output: serde_json::to_string_pretty(&json!({
"provider": provider,
"models": models,
"example": "Use action 'set' with this provider and a model ID to switch"
}))?,
error: None,
})
}
}
+4
View File
@@ -566,6 +566,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
agents.insert(
@@ -580,6 +582,8 @@ mod tests {
agentic: false,
allowed_tools: Vec::new(),
max_iterations: 10,
timeout_secs: None,
agentic_timeout_secs: None,
},
);
agents
+84
View File
@@ -281,4 +281,88 @@ mod tests {
// Tool should now be activated
assert!(activated.lock().unwrap().is_activated("fs__read"));
}
/// Verify tool_search works with stubs from multiple MCP servers,
/// simulating a daemon-mode setup where several servers are deferred.
#[tokio::test]
async fn multiple_servers_stubs_all_searchable() {
let activated = Arc::new(Mutex::new(ActivatedToolSet::new()));
let stubs = vec![
make_stub("server_a__list_files", "List files on server A"),
make_stub("server_a__read_file", "Read file on server A"),
make_stub("server_b__query_db", "Query database on server B"),
make_stub("server_b__insert_row", "Insert row on server B"),
];
let tool = ToolSearchTool::new(make_deferred_set(stubs).await, Arc::clone(&activated));
// Search should find tools across both servers
let result = tool
.execute(serde_json::json!({"query": "file"}))
.await
.unwrap();
assert!(result.success);
assert!(result.output.contains("server_a__list_files"));
assert!(result.output.contains("server_a__read_file"));
// Server B tools should also be searchable
let result = tool
.execute(serde_json::json!({"query": "database query"}))
.await
.unwrap();
assert!(result.success);
assert!(result.output.contains("server_b__query_db"));
}
/// Verify select mode activates tools and they stay activated across calls,
/// matching the daemon-mode pattern where a single ActivatedToolSet persists.
#[tokio::test]
async fn select_activates_and_persists_across_calls() {
let activated = Arc::new(Mutex::new(ActivatedToolSet::new()));
let stubs = vec![
make_stub("srv__tool_a", "Tool A"),
make_stub("srv__tool_b", "Tool B"),
];
let tool = ToolSearchTool::new(make_deferred_set(stubs).await, Arc::clone(&activated));
// Activate tool_a
let result = tool
.execute(serde_json::json!({"query": "select:srv__tool_a"}))
.await
.unwrap();
assert!(result.success);
assert!(activated.lock().unwrap().is_activated("srv__tool_a"));
assert!(!activated.lock().unwrap().is_activated("srv__tool_b"));
// Activate tool_b in a separate call
let result = tool
.execute(serde_json::json!({"query": "select:srv__tool_b"}))
.await
.unwrap();
assert!(result.success);
// Both should remain activated
let guard = activated.lock().unwrap();
assert!(guard.is_activated("srv__tool_a"));
assert!(guard.is_activated("srv__tool_b"));
assert_eq!(guard.tool_specs().len(), 2);
}
/// Verify re-activating an already-activated tool does not duplicate it.
#[tokio::test]
async fn reactivation_is_idempotent() {
let activated = Arc::new(Mutex::new(ActivatedToolSet::new()));
let tool = ToolSearchTool::new(
make_deferred_set(vec![make_stub("srv__tool", "A tool")]).await,
Arc::clone(&activated),
);
tool.execute(serde_json::json!({"query": "select:srv__tool"}))
.await
.unwrap();
tool.execute(serde_json::json!({"query": "select:srv__tool"}))
.await
.unwrap();
assert_eq!(activated.lock().unwrap().tool_specs().len(), 1);
}
}
+59
View File
@@ -0,0 +1,59 @@
# English tool descriptions (default locale)
#
# Each key under [tools] matches the tool's name() return value.
# Values are the human-readable descriptions shown in system prompts.
[tools]
backup = "Create, list, verify, and restore workspace backups"
browser = "Web/browser automation with pluggable backends (agent-browser, rust-native, computer_use). Supports DOM actions plus optional OS-level actions (mouse_move, mouse_click, mouse_drag, key_type, key_press, screen_capture) through a computer-use sidecar. Use 'snapshot' to map interactive elements to refs (@e1, @e2). Enforces browser.allowed_domains for open actions."
browser_delegate = "Delegate browser-based tasks to a browser-capable CLI for interacting with web applications like Teams, Outlook, Jira, Confluence"
browser_open = "Open an approved HTTPS URL in the system browser. Security constraints: allowlist-only domains, no local/private hosts, no scraping."
cloud_ops = "Cloud transformation advisory tool. Analyzes IaC plans, assesses migration paths, reviews costs, and checks architecture against Well-Architected Framework pillars. Read-only: does not create or modify cloud resources."
cloud_patterns = "Cloud pattern library. Given a workload description, suggests applicable cloud-native architectural patterns (containerization, serverless, database modernization, etc.)."
composio = "Execute actions on 1000+ apps via Composio (Gmail, Notion, GitHub, Slack, etc.). Use action='list' to see available actions (includes parameter names). action='execute' with action_name/tool_slug and params to run an action. If you are unsure of the exact params, pass 'text' instead with a natural-language description of what you want (Composio will resolve the correct parameters via NLP). action='list_accounts' or action='connected_accounts' to list OAuth-connected accounts. action='connect' with app/auth_config_id to get OAuth URL. connected_account_id is auto-resolved when omitted."
content_search = "Search file contents by regex pattern within the workspace. Supports ripgrep (rg) with grep fallback. Output modes: 'content' (matching lines with context), 'files_with_matches' (file paths only), 'count' (match counts per file). Example: pattern='fn main', include='*.rs', output_mode='content'."
cron_add = """Create a scheduled cron job (shell or agent) with cron/at/every schedules. Use job_type='agent' with a prompt to run the AI agent on schedule. To deliver output to a channel (Discord, Telegram, Slack, Mattermost, Matrix), set delivery={"mode":"announce","channel":"discord","to":"<channel_id_or_chat_id>"}. This is the preferred tool for sending scheduled/delayed messages to users via channels."""
cron_list = "List all scheduled cron jobs"
cron_remove = "Remove a cron job by id"
cron_run = "Force-run a cron job immediately and record run history"
cron_runs = "List recent run history for a cron job"
cron_update = "Patch an existing cron job (schedule, command, prompt, enabled, delivery, model, etc.)"
data_management = "Workspace data retention, purge, and storage statistics"
delegate = "Delegate a subtask to a specialized agent. Use when: a task benefits from a different model (e.g. fast summarization, deep reasoning, code generation). The sub-agent runs a single prompt by default; with agentic=true it can iterate with a filtered tool-call loop."
file_edit = "Edit a file by replacing an exact string match with new content"
file_read = "Read file contents with line numbers. Supports partial reading via offset and limit. Extracts text from PDF; other binary files are read with lossy UTF-8 conversion."
file_write = "Write contents to a file in the workspace"
git_operations = "Perform structured Git operations (status, diff, log, branch, commit, add, checkout, stash). Provides parsed JSON output and integrates with security policy for autonomy controls."
glob_search = "Search for files matching a glob pattern within the workspace. Returns a sorted list of matching file paths relative to the workspace root. Examples: '**/*.rs' (all Rust files), 'src/**/mod.rs' (all mod.rs in src)."
google_workspace = "Interact with Google Workspace services (Drive, Gmail, Calendar, Sheets, Docs, etc.) via the gws CLI. Requires gws to be installed and authenticated."
hardware_board_info = "Return full board info (chip, architecture, memory map) for connected hardware. Use when: user asks for 'board info', 'what board do I have', 'connected hardware', 'chip info', 'what hardware', or 'memory map'."
hardware_memory_map = "Return the memory map (flash and RAM address ranges) for connected hardware. Use when: user asks for 'upper and lower memory addresses', 'memory map', 'address space', or 'readable addresses'. Returns flash/RAM ranges from datasheets."
hardware_memory_read = "Read actual memory/register values from Nucleo via USB. Use when: user asks to 'read register values', 'read memory at address', 'dump memory', 'lower memory 0-126', or 'give address and value'. Returns hex dump. Requires Nucleo connected via USB and probe feature. Params: address (hex, e.g. 0x20000000 for RAM start), length (bytes, default 128)."
http_request = "Make HTTP requests to external APIs. Supports GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS methods. Security constraints: allowlist-only domains, no local/private hosts, configurable timeout and response size limits."
image_info = "Read image file metadata (format, dimensions, size) and optionally return base64-encoded data."
knowledge = "Manage a knowledge graph of architecture decisions, solution patterns, lessons learned, and experts. Actions: capture, search, relate, suggest, expert_find, lessons_extract, graph_stats."
linkedin = "Manage LinkedIn: create posts, list your posts, comment, react, delete posts, view engagement, get profile info, and read the configured content strategy. Requires LINKEDIN_* credentials in .env file."
memory_forget = "Remove a memory by key. Use to delete outdated facts or sensitive data. Returns whether the memory was found and removed."
memory_recall = "Search long-term memory for relevant facts, preferences, or context. Returns scored results ranked by relevance."
memory_store = "Store a fact, preference, or note in long-term memory. Use category 'core' for permanent facts, 'daily' for session notes, 'conversation' for chat context, or a custom category name."
microsoft365 = "Microsoft 365 integration: manage Outlook mail, Teams messages, Calendar events, OneDrive files, and SharePoint search via Microsoft Graph API"
model_routing_config = "Manage default model settings, scenario-based provider/model routes, classification rules, and delegate sub-agent profiles"
notion = "Interact with Notion: query databases, read/create/update pages, and search the workspace."
pdf_read = "Extract plain text from a PDF file in the workspace. Returns all readable text. Image-only or encrypted PDFs return an empty result. Requires the 'rag-pdf' build feature."
project_intel = "Project delivery intelligence: generate status reports, detect risks, draft client updates, summarize sprints, and estimate effort. Read-only analysis tool."
proxy_config = "Manage ZeroClaw proxy settings (scope: environment | zeroclaw | services), including runtime and process env application"
pushover = "Send a Pushover notification to your device. Requires PUSHOVER_TOKEN and PUSHOVER_USER_KEY in .env file."
schedule = """Manage scheduled shell-only tasks. Actions: create/add/once/list/get/cancel/remove/pause/resume. WARNING: This tool creates shell jobs whose output is only logged, NOT delivered to any channel. To send a scheduled message to Discord/Telegram/Slack/Matrix, use the cron_add tool with job_type='agent' and a delivery config like {"mode":"announce","channel":"discord","to":"<channel_id>"}."""
screenshot = "Capture a screenshot of the current screen. Returns the file path and base64-encoded PNG data."
security_ops = "Security operations tool for managed cybersecurity services. Actions: triage_alert (classify/prioritize alerts), run_playbook (execute incident response steps), parse_vulnerability (parse scan results), generate_report (create security posture reports), list_playbooks (list available playbooks), alert_stats (summarize alert metrics)."
shell = "Execute a shell command in the workspace directory"
sop_advance = "Report the result of the current SOP step and advance to the next step. Provide the run_id, whether the step succeeded or failed, and a brief output summary."
sop_approve = "Approve a pending SOP step that is waiting for operator approval. Returns the step instruction to execute. Use sop_status to see which runs are waiting."
sop_execute = "Manually trigger a Standard Operating Procedure (SOP) by name. Returns the run ID and first step instruction. Use sop_list to see available SOPs."
sop_list = "List all loaded Standard Operating Procedures (SOPs) with their triggers, priority, step count, and active run count. Optionally filter by name or priority."
sop_status = "Query SOP execution status. Provide run_id for a specific run, or sop_name to list runs for that SOP. With no arguments, shows all active runs."
swarm = "Orchestrate a swarm of agents to collaboratively handle a task. Supports sequential (pipeline), parallel (fan-out/fan-in), and router (LLM-selected) strategies."
tool_search = """Fetch full schema definitions for deferred MCP tools so they can be called. Use "select:name1,name2" for exact match or keywords to search."""
web_fetch = "Fetch a web page and return its content as clean plain text. HTML pages are automatically converted to readable text. JSON and plain text responses are returned as-is. Only GET requests; follows redirects. Security: allowlist-only domains, no local/private hosts."
web_search_tool = "Search the web for information. Returns relevant search results with titles, URLs, and descriptions. Use this to find current information, news, or research topics."
workspace = "Manage multi-client workspaces. Subcommands: list, switch, create, info, export. Each workspace provides isolated memory, audit, secrets, and tool restrictions."
+60
View File
@@ -0,0 +1,60 @@
# 中文工具描述 (简体中文)
#
# [tools] 下的每个键对应工具的 name() 返回值。
# 值是显示在系统提示中的人类可读描述。
# 缺少的键将回退到英文 (en.toml) 描述。
[tools]
backup = "创建、列出、验证和恢复工作区备份"
browser = "基于可插拔后端(agent-browser、rust-native、computer_use)的网页/浏览器自动化。支持 DOM 操作以及通过 computer-use 辅助工具进行的可选系统级操作(mouse_move、mouse_click、mouse_drag、key_type、key_press、screen_capture)。使用 'snapshot' 将交互元素映射到引用(@e1、@e2)。对 open 操作强制执行 browser.allowed_domains。"
browser_delegate = "将基于浏览器的任务委派给具有浏览器功能的 CLI,用于与 Teams、Outlook、Jira、Confluence 等 Web 应用交互"
browser_open = "在系统浏览器中打开经批准的 HTTPS URL。安全约束:仅允许列表域名,禁止本地/私有主机,禁止抓取。"
cloud_ops = "云转型咨询工具。分析 IaC 计划、评估迁移路径、审查成本,并根据良好架构框架支柱检查架构。只读:不创建或修改云资源。"
cloud_patterns = "云模式库。根据工作负载描述,建议适用的云原生架构模式(容器化、无服务器、数据库现代化等)。"
composio = "通过 Composio 在 1000 多个应用上执行操作(Gmail、Notion、GitHub、Slack 等)。使用 action='list' 查看可用操作(包含参数名称)。使用 action='execute' 配合 action_name/tool_slug 和 params 运行操作。如果不确定具体参数,可传入 'text' 并用自然语言描述需求(Composio 将通过 NLP 解析正确参数)。使用 action='list_accounts' 或 action='connected_accounts' 列出 OAuth 已连接账户。使用 action='connect' 配合 app/auth_config_id 获取 OAuth URL。省略时自动解析 connected_account_id。"
content_search = "在工作区内按正则表达式搜索文件内容。支持 ripgrep (rg),可回退到 grep。输出模式:'content'(带上下文的匹配行)、'files_with_matches'(仅文件路径)、'count'(每个文件的匹配计数)。"
cron_add = "创建带有 cron/at/every 计划的定时任务(shell 或 agent)。使用 job_type='agent' 配合 prompt 按计划运行 AI 代理。要将输出发送到频道(Discord、Telegram、Slack、Mattermost、Matrix),请设置 delivery 配置。这是通过频道向用户发送定时/延迟消息的首选工具。"
cron_list = "列出所有已计划的 cron 任务"
cron_remove = "按 ID 删除 cron 任务"
cron_run = "立即强制运行 cron 任务并记录运行历史"
cron_runs = "列出 cron 任务的最近运行历史"
cron_update = "修改现有 cron 任务(计划、命令、提示、启用状态、投递配置、模型等)"
data_management = "工作区数据保留、清理和存储统计"
delegate = "将子任务委派给专用代理。适用场景:任务受益于不同模型(如快速摘要、深度推理、代码生成)。子代理默认运行单个提示;设置 agentic=true 后可通过过滤的工具调用循环进行迭代。"
file_edit = "通过替换精确匹配的字符串来编辑文件"
file_read = "读取带行号的文件内容。支持通过 offset 和 limit 进行部分读取。可从 PDF 提取文本;其他二进制文件使用有损 UTF-8 转换读取。"
file_write = "将内容写入工作区中的文件"
git_operations = "执行结构化的 Git 操作(status、diff、log、branch、commit、add、checkout、stash)。提供解析后的 JSON 输出,并与安全策略集成以实现自主控制。"
glob_search = "在工作区内搜索匹配 glob 模式的文件。返回相对于工作区根目录的排序文件路径列表。示例:'**/*.rs'(所有 Rust 文件)、'src/**/mod.rs'src 中所有 mod.rs)。"
google_workspace = "与 Google Workspace 服务(Drive、Gmail、Calendar、Sheets、Docs 等)交互。通过 gws CLI 操作,需要 gws 已安装并认证。"
hardware_board_info = "返回已连接硬件的完整板卡信息(芯片、架构、内存映射)。适用场景:用户询问板卡信息、连接的硬件、芯片信息等。"
hardware_memory_map = "返回已连接硬件的内存映射(Flash 和 RAM 地址范围)。适用场景:用户询问内存地址、地址空间或可读地址。返回数据手册中的 Flash/RAM 范围。"
hardware_memory_read = "通过 USB 从 Nucleo 读取实际内存/寄存器值。适用场景:用户要求读取寄存器值、读取内存地址、转储内存等。返回十六进制转储。需要 Nucleo 通过 USB 连接并启用 probe 功能。"
http_request = "向外部 API 发送 HTTP 请求。支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 方法。安全约束:仅允许列表域名,禁止本地/私有主机,可配置超时和响应大小限制。"
image_info = "读取图片文件元数据(格式、尺寸、大小),可选返回 base64 编码数据。"
knowledge = "管理架构决策、解决方案模式、经验教训和专家的知识图谱。操作:capture、search、relate、suggest、expert_find、lessons_extract、graph_stats。"
linkedin = "管理 LinkedIn:创建帖子、列出帖子、评论、点赞、删除帖子、查看互动数据、获取个人资料信息,以及阅读配置的内容策略。需要在 .env 文件中配置 LINKEDIN_* 凭据。"
memory_forget = "按键删除记忆。用于删除过时事实或敏感数据。返回记忆是否被找到并删除。"
memory_recall = "在长期记忆中搜索相关事实、偏好或上下文。返回按相关性排名的评分结果。"
memory_store = "在长期记忆中存储事实、偏好或笔记。使用类别 'core' 存储永久事实,'daily' 存储会话笔记,'conversation' 存储聊天上下文,或使用自定义类别名称。"
microsoft365 = "Microsoft 365 集成:通过 Microsoft Graph API 管理 Outlook 邮件、Teams 消息、日历事件、OneDrive 文件和 SharePoint 搜索"
model_routing_config = "管理默认模型设置、基于场景的提供商/模型路由、分类规则和委派子代理配置"
notion = "与 Notion 交互:查询数据库、读取/创建/更新页面、搜索工作区。"
pdf_read = "从工作区中的 PDF 文件提取纯文本。返回所有可读文本。仅图片或加密的 PDF 返回空结果。需要 'rag-pdf' 构建功能。"
project_intel = "项目交付智能:生成状态报告、检测风险、起草客户更新、总结冲刺、估算工作量。只读分析工具。"
proxy_config = "管理 ZeroClaw 代理设置(范围:environment | zeroclaw | services),包括运行时和进程环境应用"
pushover = "向设备发送 Pushover 通知。需要在 .env 文件中配置 PUSHOVER_TOKEN 和 PUSHOVER_USER_KEY。"
schedule = "管理仅限 shell 的定时任务。操作:create/add/once/list/get/cancel/remove/pause/resume。警告:此工具创建的 shell 任务输出仅记录日志,不会发送到任何频道。要向 Discord/Telegram/Slack/Matrix 发送定时消息,请使用 cron_add 工具。"
screenshot = "捕获当前屏幕截图。返回文件路径和 base64 编码的 PNG 数据。"
security_ops = "托管网络安全服务的安全运营工具。操作:triage_alert(分类/优先级排序警报)、run_playbook(执行事件响应步骤)、parse_vulnerability(解析扫描结果)、generate_report(创建安全态势报告)、list_playbooks(列出可用剧本)、alert_stats(汇总警报指标)。"
shell = "在工作区目录中执行 shell 命令"
sop_advance = "报告当前 SOP 步骤的结果并前进到下一步。提供 run_id、步骤是否成功或失败,以及简短的输出摘要。"
sop_approve = "批准等待操作员批准的待处理 SOP 步骤。返回要执行的步骤指令。使用 sop_status 查看哪些运行正在等待。"
sop_execute = "按名称手动触发标准操作程序 (SOP)。返回运行 ID 和第一步指令。使用 sop_list 查看可用 SOP。"
sop_list = "列出所有已加载的标准操作程序 (SOP),包括触发器、优先级、步骤数和活跃运行数。可按名称或优先级筛选。"
sop_status = "查询 SOP 执行状态。提供 run_id 查看特定运行,或提供 sop_name 列出该 SOP 的所有运行。无参数时显示所有活跃运行。"
swarm = "编排代理群以协作处理任务。支持顺序(管道)、并行(扇出/扇入)和路由器(LLM 选择)策略。"
tool_search = "获取延迟 MCP 工具的完整 schema 定义以便调用。使用 \"select:name1,name2\" 精确匹配或关键词搜索。"
web_fetch = "获取网页并以纯文本形式返回内容。HTML 页面自动转换为可读文本。JSON 和纯文本响应按原样返回。仅 GET 请求;跟随重定向。安全:仅允许列表域名,禁止本地/私有主机。"
web_search_tool = "搜索网络获取信息。返回包含标题、URL 和描述的相关搜索结果。用于查找当前信息、新闻或研究主题。"
workspace = "管理多客户端工作区。子命令:list、switch、create、info、export。每个工作区提供隔离的记忆、审计、密钥和工具限制。"
+37 -1
View File
@@ -16,6 +16,7 @@ import Pairing from './pages/Pairing';
import { AuthProvider, useAuth } from './hooks/useAuth';
import { DraftContext, useDraftStore } from './hooks/useDraft';
import { setLocale, type Locale } from './lib/i18n';
import { getAdminPairCode } from './lib/api';
// Locale context
interface LocaleContextType {
@@ -89,6 +90,26 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
const [code, setCode] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [displayCode, setDisplayCode] = useState<string | null>(null);
const [codeLoading, setCodeLoading] = useState(true);
// Fetch the current pairing code from the admin endpoint (localhost only)
useEffect(() => {
let cancelled = false;
getAdminPairCode()
.then((data) => {
if (!cancelled && data.pairing_code) {
setDisplayCode(data.pairing_code);
}
})
.catch(() => {
// Admin endpoint not reachable (non-localhost) — user must check terminal
})
.finally(() => {
if (!cancelled) setCodeLoading(false);
});
return () => { cancelled = true; };
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -120,8 +141,23 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
style={{ boxShadow: '0 0 30px rgba(0,128,255,0.3)' }}
/>
<h1 className="text-2xl font-bold text-gradient-blue mb-2">ZeroClaw</h1>
<p className="text-[#556080] text-sm">Enter the pairing code from your terminal</p>
{displayCode ? (
<p className="text-[#556080] text-sm">Your pairing code</p>
) : (
<p className="text-[#556080] text-sm">Enter the pairing code from your terminal</p>
)}
</div>
{/* Show the pairing code if available (localhost) */}
{!codeLoading && displayCode && (
<div className="mb-6 p-4 rounded-xl text-center" style={{ background: 'rgba(0,128,255,0.08)', border: '1px solid rgba(0,128,255,0.2)' }}>
<div className="text-4xl font-mono font-bold tracking-[0.4em] text-white py-2">
{displayCode}
</div>
<p className="text-[#556080] text-xs mt-2">Enter this code below or on another device</p>
</div>
)}
<form onSubmit={handleSubmit}>
<input
type="text"
+8
View File
@@ -93,6 +93,14 @@ export async function pair(code: string): Promise<{ token: string }> {
return data;
}
export async function getAdminPairCode(): Promise<{ pairing_code: string | null; pairing_required: boolean }> {
const response = await fetch('/admin/paircode');
if (!response.ok) {
throw new Error(`Failed to fetch pairing code (${response.status})`);
}
return response.json() as Promise<{ pairing_code: string | null; pairing_required: boolean }>;
}
// ---------------------------------------------------------------------------
// Public health (no auth required)
// ---------------------------------------------------------------------------
+14
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { getAdminPairCode } from '../lib/api';
interface Device {
id: string;
@@ -33,6 +34,19 @@ export default function Pairing() {
}
}, [token]);
// Fetch the current pairing code on mount (if one is active)
useEffect(() => {
getAdminPairCode()
.then((data) => {
if (data.pairing_code) {
setPairingCode(data.pairing_code);
}
})
.catch(() => {
// Admin endpoint not reachable — code will show after clicking "Pair New Device"
});
}, []);
useEffect(() => {
fetchDevices();
}, [fetchDevices]);