Compare commits

...

219 Commits

Author SHA1 Message Date
Argenis
feaca20582
feat: desktop companion app + device-aware installer + CI/CD (#4500)
* feat: add desktop companion app integration and CI/CD pipeline

- Add `zeroclaw desktop` CLI command to launch/install companion app
- Add device-aware installer (desktop/server/mobile/embedded/container)
- Replace from-source Tauri build with pre-built .dmg download flow
- Add `build-desktop` job to beta and stable release workflows
- Build universal macOS .dmg via Tauri on macos-14 runners
- Include .dmg in GitHub Release assets alongside CLI binaries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump version to 0.6.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update Cargo.lock for 0.6.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 03:59:11 -04:00
Nim G
40af505e90
feat(security): wire LeakDetector into outbound message path (#4457)
* feat(security): wire LeakDetector into outbound message path

Wire LeakDetector.scan() into channel response sanitization (via
sanitize_channel_response) and cron job delivery (via
deliver_announcement) to prevent credential leakage to external
channels.

Changes:
- src/channels/mod.rs: Add leak detection to sanitize_channel_response()
  with tracing::warn! on detection. Follows prior art from commit fb3b7b8e.
- src/cron/scheduler.rs: Add leak detection to deliver_announcement()
  before sending output to any channel (Telegram, Discord, Slack,
  Mattermost, Signal, Matrix, WhatsApp, QQ).
- Added unit tests in both modules to verify redaction behavior.

All outbound channel messages and cron job delivery are now scanned for
API keys, AWS credentials, JWTs, database URLs, private keys, and
high-entropy tokens. Detected leaks are redacted before delivery and
logged via tracing::warn! with pattern details.

Ref: SPEC-1.3-leak-detector-outbound.md (gap closure §1.3)
Prior art: commit fb3b7b8e (zeroclaw_homecomming branch)

* fix(security): enforce leak redaction with RedactedOutput newtype

Replace raw String return from scan_and_redact_output with a
RedactedOutput newtype. All channel dispatch in deliver_announcement
must go through this type, so the compiler rejects any path that
skips leak detection. Replaces tests that called LeakDetector in
isolation with tests that exercise scan_and_redact_output through
the RedactedOutput boundary.
2026-03-24 01:43:49 -04:00
Nim G
f812dbcb85
feat(channels): add message redaction API to Channel trait (#4458)
* feat(channels): add message redaction API to Channel trait

Add redact_message() method to Channel trait with default no-op implementation.
Implement for MatrixChannel using existing room.redact() SDK method.
Add comprehensive test coverage for trait default and Matrix implementation.

Changes:
- src/channels/traits.rs: Add redact_message() default method and test
- src/channels/matrix.rs: Implement redact_message() for MatrixChannel
- tests/integration/channel_matrix.rs: Add RedactMessage event, test channel impl, and lifecycle test

Tests added:
- default_redact_message_returns_success (trait default)
- redact_message_lifecycle (MatrixTestChannel)
- Updated minimal_channel_all_defaults_succeed

All tests pass. No breaking changes.

* test(channels): clarify redact_message test coverage

* fix(channels): use OwnedRoomId for matrix redact_message room lookup

RoomId is unsized (wraps str), so inlining .parse() in
get_room(&target_room.parse()?) fails to compile with the
channel-matrix feature. Use an intermediate OwnedRoomId binding,
matching the pattern used in send() and listen().
2026-03-24 01:43:41 -04:00
Nim G
59225d97b3
feat(memory): add bulk memory deletion by namespace and session (#4459)
Add purge_namespace() and purge_session() methods to Memory trait with
default bail! implementations. Implement both in SQLite backend using
spawn_blocking pattern matching forget(). Expose via new memory_purge
tool with security enforcement.

Changes:
- src/memory/traits.rs: Add purge_namespace/purge_session trait methods
- src/memory/sqlite.rs: Implement purge methods + 8 backend tests
- src/tools/memory_purge.rs: New tool following memory_forget pattern
- src/tools/mod.rs: Register memory_purge module and export

Tests:
- SQLite backend: 8 tests (namespace/session purge, preservation, count, noop)
- Tool: 8 tests (name/schema, purge success, noop, missing param, readonly/rate-limit)

All tests pass. Clippy clean. Formatted with cargo fmt.
2026-03-24 01:43:39 -04:00
Nim G
483f773e1d
feat(ci): add per-component path labels and labeler workflow (#4461)
* feat(ci): add per-component labels for channels, providers, and tools

labeler.yml only had base scope labels (channel, provider, tool) matching
entire directories. The PR workflow and ci-map specs already describe
fine-grained module labels (channel:telegram, provider:kimi, tool:shell)
but the labeler config never implemented them.

Adds 27 channel, 13 provider, and 12 tool group entries. Base labels are
preserved for the compaction rule in pr-labeler.yml.

* feat(ci): add pr-path-labeler workflow to invoke actions/labeler

labeler.yml has path-label config but no workflow was invoking it.
Adds pr-path-labeler.yml on pull_request_target to apply path and
per-component labels automatically. Updates actions source policy
doc to reflect the new workflow and action.

* docs(contributing): add label registry as single reference for all PR labels

Label definitions were scattered across labeler.yml, label-policy.json,
pr-workflow.md, and ci-map.md with no consolidated view. Adds a single
registry documenting all 99 labels by category, their definitions, how
each is applied, and which automation exists vs. is specced but missing.

* docs(contributing): correct label-registry automation status

The CI was simplified from 20+ workflows to 4. Size, risk,
contributor tier, and triage label automation (pr-labeler.yml,
pr-auto-response.yml, pr-check-stale.yml) were removed during
that simplification. Update the registry to reflect that these
labels are now applied manually, not "not yet implemented."

See PR #2931 for the upstream docs cleanup.
2026-03-24 01:43:35 -04:00
Argenis
b4bbe820a2
feat(memory): add pgvector support and Postgres knowledge graph (#4028) (#4488)
* feat(memory): add pgvector support and Postgres knowledge graph (#4028)

Enhance PostgresMemory with optional pgvector extension for hybrid
vector+keyword recall. Add namespace and importance columns. Create
PgKnowledgeGraph with recursive CTEs for graph traversal (no AGE
dependency). Extend ConsolidationResult with facts/trend fields for
richer extraction. All behind memory-postgres feature flag.

New file: knowledge_graph_pg.rs (5 unit tests)
Modified: postgres.rs (pgvector init, namespace/importance columns),
consolidation.rs (facts/trend fields), StorageProviderConfig

* fix: update PostgresMemory::new call in CLI with pgvector params
2026-03-24 01:04:01 -04:00
Argenis
1702bb2747
fix: route WebSocket connections through configured proxy (#4408)
* fix: route WebSocket connections through configured proxy (#4408)

tokio_tungstenite::connect_async does not honour proxy settings. Add
proxy-aware WebSocket connect helpers (HTTP CONNECT and SOCKS5) in
config::schema and update all six channel WebSocket connections
(discord, discord_history, slack, dingtalk, lark, qq) to use
ws_connect_with_proxy instead of connect_async.

* fix: update Cargo.lock with tokio-socks dependency
2026-03-24 01:02:46 -04:00
Argenis
b6f661c3c5
feat(agent): add complexity-based eval and auto-classify routing (#3928) (#4486)
Add heuristic complexity estimation (Simple/Standard/Complex) and
post-response quality evaluation to enable cheapest-capable model
routing. When rule-based classification misses, auto_classify falls
back to complexity-tier hints for model selection. Eval gate checks
response quality and suggests retry with higher-tier model if below
threshold.

New file: eval.rs (14 unit tests)
Modified: AgentConfig (eval, auto_classify fields), classify_model()
2026-03-24 00:51:18 -04:00
Argenis
ac543cff20
fix(tools): allow git_operations to run in workspace subdirectories (#4483)
Fixes #4409. The git_operations tool now accepts an optional 'path'
parameter to specify a subdirectory within the workspace. Path
traversal outside the workspace is rejected for security.
2026-03-24 00:44:41 -04:00
Argenis
c88affa020
fix(cron): treat empty allowed_tools as None and sanitize tool names (#4482)
Fixes #4442. Empty allowed_tools arrays caused all tools to be filtered
out, making cron jobs fail. Also adds tool name validation in the
OpenRouter provider to skip tools with names that violate the OpenAI
naming regex.
2026-03-24 00:44:39 -04:00
Argenis
67998ad702
fix(docker): add [autonomy] section with auto_approve to Docker configs (#4481)
Fixes #4445. Docker/podman containers running in daemon mode had no
[autonomy] section in the baked config.toml, causing file_write and
file_edit tools to be auto-denied in non-interactive mode.
2026-03-24 00:44:32 -04:00
Argenis
c104b23ddb
feat(agent): add token efficiency analyzer layer (#3892) (#4484)
Add history pruning and context-aware tool filtering to reduce token
usage per API call. History pruner collapses old tool call/result pairs
and drops oldest messages when over budget. Context analyzer suggests
relevant tools per iteration based on keyword matching.

New files: history_pruner.rs, context_analyzer.rs
Modified: AgentConfig (new fields), ToolFilterGroup (filter_builtins),
agent loop (pruning integration)
2026-03-24 00:38:15 -04:00
Argenis
698adca707
fix: add channel-lark to default features (#4472)
Resolves #3540. Lark/Feishu channel was not included in default features, causing builds via cargo install, brew, and other standard install methods to produce binaries without Lark support. Also fixes pre-existing lark audio test failures that were previously hidden.
2026-03-24 00:23:43 -04:00
Argenis
50a877b4c1
fix(ci): use shared concurrency group for master push events (#4479)
The concurrency group used github.sha for push events, giving each
merge its own group. Back-to-back merges queued multiple full CI runs
that never cancelled each other, exhausting runner capacity.

Use a fixed 'push-master' group so only the latest push to master
runs CI — older runs are cancelled automatically.
2026-03-24 00:15:26 -04:00
Argenis
fa7b615508
feat(providers): support Bearer token API keys for Amazon Bedrock (#4473)
Add BedrockAuth enum supporting both SigV4 and BearerToken authentication.
BEDROCK_API_KEY env var takes precedence over AWS AKSK credentials.
When a Bearer token is provided, requests use Authorization: Bearer header
instead of SigV4 signing. Existing SigV4 auth remains fully backward
compatible.

Closes #3742
2026-03-24 00:08:13 -04:00
Argenis
ea9eccfe8b
feat: make shell tool timeout configurable via config.toml (#4468)
Add `[shell_tool]` section to config schema with `timeout_secs` field
(default 60s). The ShellTool struct now carries the timeout as an
instance field instead of relying on the hardcoded constant, and the
full tool registry reads the value from Config at construction time.

Closes #4331
2026-03-24 00:07:46 -04:00
Argenis
eb036b4d95
feat(skills): add TEST.sh validation/testing framework (#4476)
Add a `zeroclaw skills test [name] [--verbose]` command that discovers
and executes TEST.sh files inside skill directories. Each line in a
TEST.sh follows the format `command | expected_exit_code | pattern` and
is validated against actual exit code and output (substring or regex).

- Add `Test` variant to `SkillCommands` enum in lib.rs
- Create `src/skills/testing.rs` with test runner, parser, and
  pretty-printed results using the console crate
- Wire the new subcommand into `handle_command()` in skills/mod.rs
- Add example TEST.sh for the browser skill
- Include 14 unit tests covering parsing, pattern matching, execution,
  aggregation, and edge cases

Closes #3697
2026-03-23 23:58:52 -04:00
Argenis
1c07d5b411
feat(tools): add built-in ask_user tool for interactive prompts (#4474)
Implement a new ask_user tool that sends a question to a messaging
channel and waits for the user's response with configurable timeout.
Supports optional structured choices and works across all channels
(Telegram, Discord, Slack, CLI, Web) via the late-bound channel map
pattern used by PollTool and ReactionTool.

Closes #3698
2026-03-23 23:58:50 -04:00
Argenis
0fe3834349
feat(channels): migrate inline transcription to TranscriptionManager (#4475)
Migrate Telegram, Discord, Slack, WhatsApp Web, and Matrix channels
from inline TranscriptionConfig/transcribe_audio() calls to the
centralized TranscriptionManager pattern for unified provider selection,
fallback, and global cap enforcement.

Each channel now:
- Stores a transcription_manager: Option<Arc<TranscriptionManager>> field
- Builds the manager in with_transcription() from config
- Calls manager.transcribe() instead of the legacy transcribe_audio() shim
- Matrix replaces the whisper-cpp shell-out with the manager API

Closes #4311
2026-03-23 23:58:05 -04:00
Argenis
33f9d66b54
feat(channel): add transcribe_non_ptt_audio config for WhatsApp STT (#4470)
Allow transcription of forwarded/regular audio messages on WhatsApp,
not just push-to-talk voice notes. Controlled by the new
`transcribe_non_ptt_audio` option in `[transcription]` (default: false).

Closes #4343
2026-03-23 23:43:28 -04:00
Argenis
368f39829f
fix: strip [Used tools: xxx] logs from channel responses (#4400)
The tool-context summary (e.g. `[Used tools: browser_open]`) was
prepended to assistant history entries for all non-Telegram channels so
the LLM could retain awareness of prior tool usage.  This caused the
model to learn and reproduce the bracket format in its own output,
delivering raw log lines to end-users instead of meaningful tool results.

Three changes:

1. Stop prepending `[Used tools: …]` to stored history for all channels.
   The LLM already receives tool context through the tool-call/result
   messages built by `run_tool_call_loop`, making the summary redundant.

2. Strip any `[Used tools: …]` prefix from cached history on reload, so
   existing sessions are cleaned up retroactively.

3. Strip the prefix in `sanitize_channel_response` as a safety net, in
   case the model still echoes it from older context.
2026-03-23 23:34:29 -04:00
Argenis
9376c26018
feat(tools): add Claude Code task runner with Slack progress and SSH handoff (#4466)
Implement ClaudeCodeRunner that spawns Claude Code in a tmux session with
HTTP hooks to POST tool execution events back to ZeroClaw's gateway,
updating a Slack message in-place with progress, plus an SSH handoff link.

Components:
- src/tools/claude_code_runner.rs: session lifecycle (tmux spawn, hook
  config generation, event handling, TTL cleanup)
- src/gateway/api.rs: POST /hooks/claude-code endpoint for hook events
- src/channels/slack.rs: chat.update + chat.postMessage with ts return
- src/config/schema.rs: ClaudeCodeRunnerConfig (enabled, ssh_host,
  tmux_prefix, session_ttl)
- src/tools/mod.rs: register runner tool

Closes #4287
2026-03-23 23:24:58 -04:00
Argenis
08e131d7c6
feat(slack): add /config command with Block Kit UI for model switching (#4464)
Add interactive /config command for Slack that renders Block Kit
static_select dropdowns for provider and model selection. Interactive
block_actions payloads from Socket Mode are translated into synthetic
/models and /model commands so existing runtime command handling
applies the selection to the per-sender RouteSelectionMap.

- Enable runtime model switching for the slack channel
- Add ShowConfig variant to ChannelRuntimeCommand
- Parse /config in parse_runtime_command (gated by supports_runtime_model_switch)
- Build Block Kit JSON with provider/model dropdowns and current selection
- Detect __ZEROCLAW_BLOCK_KIT__ prefix in SlackChannel::send() to post raw blocks
- Handle interactive envelope type in Socket Mode for block_actions events
- Non-Slack channels get a plain-text /config fallback

Closes #4286
2026-03-23 23:24:56 -04:00
Argenis
36db977b35
feat: adopt AGENTS.md as the primary agent instruction format (#4462)
Replace the AGENTS.md symlink with a real file containing all cross-tool
agent instructions. Reduce CLAUDE.md to Claude Code-specific directives
only, with a reference back to AGENTS.md for shared conventions.

Closes #4289
2026-03-23 23:24:45 -04:00
Argenis
92b0ebb61a
fix: include channel-lark in Docker build defaults (#4463)
Resolves #3538. Docker builds only included memory-postgres in ZEROCLAW_CARGO_FEATURES, which meant Lark/Feishu long-connection (WebSocket) support was not compiled into container images.
2026-03-23 23:22:36 -04:00
Argenis
9c312180a2
chore: bump version to 0.6.0 (#4455)
Includes macOS desktop menu bar app, media pipeline channels,
SOP engine improvements, and CLI tool integrations.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 21:16:46 -04:00
Argenis
a433c37c53
feat: add macOS desktop menu bar app (Tauri) (#4454)
* feat(tauri): add desktop app scaffolding with Tauri commands

Add Tauri v2 workspace member under apps/tauri with gateway client,
shared state, and Tauri command handlers for status, health, channels,
pairing, and agent messaging. The desktop app communicates with the
ZeroClaw gateway over HTTP and is buildable independently via
`cargo check -p zeroclaw-desktop`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tauri): add system tray icon, menu, and events

Add tray module with menu construction (Show Dashboard, Status, Open
Gateway, Quit) and event handlers. Wire tray setup into the Tauri
builder's setup hook. Add icons/ placeholder directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tauri): add mobile entry, platform capabilities, icons, and tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tauri): add auto-pairing, gradient icons, and WebView integration

Desktop app now auto-pairs with the gateway on startup using localhost
admin endpoints, injects the token into the WebView localStorage, and
loads the dashboard from the gateway URL. Adds blue gradient Z tray
icons (idle/working/error/disconnected states), proper .icns app icon,
health polling with tray updates, and Tauri-aware API/WS/SSE paths in
the web frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tauri): add dock icon, keep-alive for tray mode, and install preflight

- Set macOS dock icon programmatically via NSApplication API so it shows
  the blue gradient Z even in dev builds without a .app bundle
- Add RunEvent::ExitRequested handler to keep app alive as a menu bar app
  when all windows are closed
- Add desktop app preflight checks to install.sh (Rust, Xcode CLT,
  cargo-tauri, Node.js) with automatic build on macOS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 20:55:22 -04:00
Argenis
af8e805016
fix(tools): correct CLI args for codex and gemini harness tools (#4401)
* feat(tools): add Codex CLI and Gemini CLI harness tools

Add two new ACP harness tools following the same pattern as ClaudeCodeTool:

- CodexCliTool: delegates to `codex -q` for OpenAI Codex CLI
- GeminiCliTool: delegates to `gemini -p` for Google Gemini CLI

Both tools share the same security model (rate limiting, act-policy
enforcement, workspace containment, env sanitisation, kill-on-drop
timeout) and are config-gated via `[codex_cli]` / `[gemini_cli]`
sections with `enabled = true`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): use 'codex exec' instead of invalid '-q' flag

* feat(tools): add OpenCode CLI harness tool

Add opencode_cli tool following the same pattern as codex_cli and
gemini_cli. Uses `opencode run "<prompt>"` for non-interactive
delegation. Includes config struct, factory registration, env
sanitization, timeout, and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): correct CLI invocation args for codex and gemini tools

- Fix codex_cli to use `codex -q` (quiet mode) instead of incorrect
  `codex exec` subcommand, matching the documented behavior and the
  actual @openai/codex CLI interface.
- Fix gemini_cli install instruction to reference `@google/gemini-cli`
  instead of the incorrect `@anthropic-ai/gemini-cli` package name.

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rareba <rareba@users.noreply.github.com>
2026-03-23 17:42:07 -04:00
Argenis
9f127f896d
fix(tools): validate task_id to prevent path traversal in delegate tool (#4405)
* feat(tools): add background and parallel execution to delegate tool

Add three new execution modes to the delegate tool:

- background: when true, spawns the sub-agent in a background tokio task
  and returns a task_id immediately. Results are persisted to
  workspace/delegate_results/{task_id}.json.

- parallel: accepts an array of agent names, runs them all concurrently
  with the same prompt, and returns all results when complete.

- action parameter with check_result/list_results/cancel_task support
  for managing background task lifecycle.

Cascade control: background sub-agents use child CancellationToken
derived from the parent, enabling cancel_all_background_tasks() to
abort all running background agents when the parent session ends.

Existing synchronous delegation flow is fully preserved (opt-in only).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): validate task_id as UUID to prevent path traversal in delegate tool

The check_result and cancel_task actions accept a user-provided task_id
that is used directly in filesystem path construction. A malicious
task_id like "../../etc/passwd" could read or overwrite arbitrary files.

Since task_ids are always generated as UUIDs internally, this adds UUID
format validation before any filesystem operations, rejecting invalid
task_id values with a clear error message.

Also updates existing tests to use valid UUID-format task_ids and adds
dedicated path traversal rejection tests.

* fix: add missing attachments field to wati ChannelMessage after media pipeline merge

* fix(channels): add missing attachments field to voice_wake and lark

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:24 -04:00
Argenis
b98971c635
fix(sop): fix state file leak and add deterministic execution tests (#4404)
* feat(sop): add deterministic execution mode with typed steps and approval checkpoints

Add opt-in deterministic execution to the SOP workflow engine, inspired
by OpenClaw's Lobster engine. Deterministic mode bypasses LLM round-trips
for step transitions, executing steps sequentially with piped outputs.

Key additions:
- SopExecutionMode::Deterministic variant and `deterministic: true` SOP.toml flag
- SopStepKind enum (Execute/Checkpoint) for marking approval pause points
- StepSchema for typed input/output validation (JSON Schema fragments)
- DeterministicRunState for persisting/resuming interrupted workflows
- DeterministicSavings for tracking LLM calls saved
- SopRunAction::DeterministicStep and CheckpointWait action variants
- SopRunStatus::PausedCheckpoint status
- Engine methods: start_deterministic_run, advance_deterministic_step,
  resume_deterministic_run, persist/load_deterministic_state
- SopConfig in config/schema.rs with sops_dir, default_execution_mode,
  max_concurrent_total, approval_timeout_secs, max_finished_runs
- Wire `pub mod sop` in lib.rs (previously dead/uncompiled module)
- Fix pre-existing test issues: TempDir import, async test annotations

All 86 SOP core tests pass (engine: 42, mod: 17, dispatch: 13, types: 14).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(sop): resolve clippy warnings and fix metrics test failures

- Add `ampersona-gates = []` feature declaration to Cargo.toml to fix
  clippy `unexpected cfg condition value` errors in sop/mod.rs,
  sop/audit.rs, and sop/metrics.rs.
- Use dynamic Utc::now() timestamps in metrics test helper `make_run()`
  instead of hardcoded 2026-02-19 dates, which had drifted outside the
  7-day/30-day windowed metric windows causing 7 test failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(sop): remove non-functional ampersona-gates feature flag

The ampersona-gates feature flag referenced ampersona_core and
ampersona_engine crates that do not exist, causing cargo check
--all-features to fail. Remove the feature flag and all gated code:

- Remove ampersona-gates from Cargo.toml [features]
- Delete src/sop/gates.rs (entire module behind cfg gate)
- Remove gated methods from audit.rs (log_gate_decision, log_phase_state)
- Remove gated MetricsProvider impl and tests from metrics.rs
- Simplify sop_status.rs gate_eval field and append_gate_status
- Update observability docs (EN + zh-CN)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(sop): fix formatting in metrics.rs

* fix(sop): wire SOP tools into tool registry

The five SOP tools (sop_list, sop_advance, sop_execute, sop_approve,
sop_status) existed as source files but were never registered in
all_tools_with_runtime. They are now conditionally registered when
sop.sops_dir is configured.

Also fixes:
- Add mod sop + SopCommands re-export to main.rs (binary crate)
- Handle new DeterministicStep/CheckpointWait variants in match arms
- Add missing struct fields (deterministic, kind, schema, llm_calls_saved)
  to test constructors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(sop): fix state file leak and add deterministic execution tests

- Fix persist_deterministic_state fallback: use system temp dir instead
  of current working directory when SOP location is unset, preventing
  state files from polluting the working directory.
- Add comprehensive test coverage for deterministic execution path:
  start, advance, checkpoint pause, completion with savings tracking,
  and rejection of non-deterministic SOPs.
- Add tests for deterministic flag in TOML manifest loading and
  checkpoint kind parsing from SOP.md.
- Add serde roundtrip tests for DeterministicRunState, SopStepKind,
  SopExecutionMode::Deterministic, and SopRunStatus::PausedCheckpoint.

* ci: retrigger CI

* fix: add missing attachments field to wati ChannelMessage after media pipeline merge

* fix(channels): add missing attachments field to voice_wake and lark

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rareba <rareba@users.noreply.github.com>
2026-03-23 16:40:26 -04:00
Argenis
2ee0229740
fix(gateway): improve WebSocket chat error handling and diagnostics (#4407)
- Add error codes to WS error messages (AGENT_INIT_FAILED, AUTH_ERROR, PROVIDER_ERROR, INVALID_JSON, UNKNOWN_MESSAGE_TYPE, EMPTY_CONTENT)
- Send Close frame with code 1011 on agent init failure
- Add tracing logs for agent init and turn errors
- Increase MAX_API_ERROR_CHARS from 200 to 500
- Frontend: handle abnormal close codes and show config error hints

Fixes #3681
Supersedes #4366
Original work by mark-linyb
2026-03-23 15:30:18 -04:00
Argenis
0d2b57ee2e
fix(channels): ensure newline between narration and draft status lines (#4394)
Narration text from native tool-call providers that doesn't end with a newline now gets one appended before dispatch to the draft updater. Prevents garbled output in Telegram drafts.

Closes #4348
2026-03-23 15:30:15 -04:00
Argenis
b85a445955
fix(channels): prevent draft streaming hang after tool loop completion (#4393)
Drop delta_tx before awaiting draft_updater so the mpsc channel closes and the updater task can terminate. Without this, draft-capable channels (e.g. Telegram) hang indefinitely after the tool loop completes.

Closes #4300
2026-03-23 15:30:12 -04:00
Argenis
dbd8c77519
feat(channels): add automatic media understanding pipeline (#4402)
* feat(channels): add automatic media understanding pipeline for inbound messages

Add MediaPipeline that pre-processes inbound channel message attachments
before the agent sees them:

- Audio: transcribed via existing transcription infrastructure, annotated
  as [Audio transcription: ...]
- Images: annotated with [Image: <file> attached] (vision-aware)
- Video: annotated with [Video: <file> attached] (placeholder for future API)

The pipeline is opt-in via [media_pipeline] config section (default: disabled).
Individual media types can be toggled independently.

Changes:
- New src/channels/media_pipeline.rs with MediaPipeline struct and tests
- New MediaPipelineConfig in config/schema.rs
- Added attachments field to ChannelMessage for media pass-through
- Wired pipeline into process_channel_message after hooks, before agent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): add attachments field to integration test fixtures

Add missing `attachments: vec![]` to all ChannelMessage struct literals
in channel_matrix.rs and channel_routing.rs after the new attachments
field was added to the struct in traits.rs.

Also fix schema.rs test compilation: make TempDir import unconditional
and add explicit type annotations on tokio::fs calls to resolve type
inference errors in the bootstrap file tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): add missing attachments field to gmail_push and discord_history constructors

These channels were added to master after the media pipeline PR was
originally branched. The ChannelMessage struct now requires an
attachments field, so initialise it to an empty Vec for channels
that do not yet extract attachments.

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:14:11 -04:00
Argenis
34db67428f
fix(gateway): stream ws agent chat responses (#4390)
Add Agent::turn_streamed() that forwards TurnEvent (Chunk, ToolCall,
ToolResult) through an mpsc channel during execution. The WebSocket
gateway uses tokio::join! to relay these events to the client in real
time instead of waiting for the full turn to complete.

Introduce chunk_reset message type so the frontend clears its draft
buffer before the authoritative done message arrives.  Update the React
AgentChat page to render streamed text live in the typing indicator
area, replacing the bounce-dot animation when content is available.

Backward-compatible: the done message still carries the full_response
field unchanged, and providers that do not support streaming fall back
to the non-streaming chat path transparently.

Closes #4372
2026-03-23 15:01:04 -04:00
Argenis
01d0c6b23a
fix(config): add cost tracking configuration to template and example files (#4387)
Add the [cost] section to dev/config.template.toml and
examples/config.example.toml documenting all CostConfig options
(enabled, daily_limit_usd, monthly_limit_usd, warn_at_percent,
allow_override) with their defaults and commented-out model pricing
examples.

Fixes #4373
2026-03-23 15:01:01 -04:00
Argenis
79f0a5ae30
fix(matrix): handle unused Result from backups().disable() (#4386)
Add `let _ =` prefix to explicitly discard the Result from
`client.encryption().backups().disable().await`, suppressing the
`unused_must_use` compiler warning.

Closes #4374
2026-03-23 15:00:58 -04:00
Argenis
5bdeeba213
feat(wati): add audio and voice message transcription (#4391)
* fix(transcription): remove deployment-specific WireGuard references from doc comments

LocalWhisperProvider and LocalWhisperConfig doc comments referenced
WireGuard and specific internal infrastructure. Deployment topology is
operator choice — replace with neutral examples.

* feat(wati): add audio and voice message transcription

* fix(wati): SSRF host validation, early fromMe check, download size cap

Validate that media_url host matches the configured api_url host before
fetching, preventing SSRF with credential leakage.  Move the fromMe
check before the HTTP download to avoid wasting bandwidth on outgoing
messages.  Add MAX_WATI_AUDIO_BYTES (25 MiB) Content-Length pre-check.
Skip empty transcripts in parse_audio_as_message.  Short-circuit
with_transcription() when config.enabled is false.

* fix(wati): remove dead field, enforce streaming size cap, log errors

- Remove unused transcription config field (clippy dead code)
- Use streaming download to enforce size cap without Content-Length
- Log network errors instead of silently swallowing with .ok()?

---------

Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
2026-03-23 14:58:21 -04:00
Argenis
b5447175ff
feat(mattermost): add audio transcription via TranscriptionManager (#4389)
* fix(transcription): remove deployment-specific WireGuard references from doc comments

LocalWhisperProvider and LocalWhisperConfig doc comments referenced
WireGuard and specific internal infrastructure. Deployment topology is
operator choice — replace with neutral examples.

* feat(mattermost): add audio transcription via TranscriptionManager

Add transcription support for Mattermost audio attachments. Routes
audio through TranscriptionManager when configured, with duration
limit enforcement and wiremock-based integration tests.

* fix(mattermost): add download size cap, HTTP status check, warn logging

Replace chained .ok()? calls in try_transcribe_audio_attachment with
explicit error handling that logs warnings on HTTP failure, non-success
status codes, and oversized files.  Add MAX_MATTERMOST_AUDIO_BYTES
(25 MiB) Content-Length pre-check.  Remove mp4 and webm from the
extension-only fallback in is_audio_file().  Short-circuit
with_transcription() when config.enabled is false.

* fix(mattermost): add [Voice] prefix, filter empty, fix config init

- Add [Voice] prefix to transcribed audio matching Slack/Discord
- Filter empty transcription results
- Only store transcription config on successful manager init
- Update test expectation for [Voice] prefix

---------

Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
2026-03-23 14:58:18 -04:00
Argenis
0dc05771ba
feat(lark): add audio message transcription (#4388)
* fix(transcription): remove deployment-specific WireGuard references from doc comments

LocalWhisperProvider and LocalWhisperConfig doc comments referenced
WireGuard and specific internal infrastructure. Deployment topology is
operator choice — replace with neutral examples.

* feat(lark): add audio message transcription

* test(channels): add three missing lark audio transcription tests

- lark_audio_skips_when_manager_none: parse_event_payload_async returns
  empty when transcription_manager is None
- lark_audio_routes_through_transcription_manager: wiremock end-to-end
  test proving file_key → download → whisper → ChannelMessage chain
- lark_audio_token_refresh_on_invalid_token_response: wiremock test
  verifying 401 triggers token invalidation and retry

Adds a #[cfg(test)] api_base_override field to LarkChannel so wiremock
can intercept requests that normally go to hardcoded Lark/Feishu API
base URLs.

* fix(lark): add audio download size cap, event_type guard, skip disabled transcription

Add MAX_LARK_AUDIO_BYTES (25 MiB) Content-Length pre-check before
reading audio response bodies on both the normal and token-refresh
paths.  Add the missing im.message.receive_v1 event_type guard in
parse_event_payload_async so non-message callbacks are rejected before
the audio branch.  Short-circuit with_transcription() when
config.enabled is false.

* fix(lark): address review feedback for audio transcription

- Wire parse_event_payload_async in webhook handler (was dead code)
- Use streaming download with enforced size cap (no Content-Length bypass)
- Fix test: lark_manager_some_when_valid_config asserted is_some with enabled=false
- Fix test: add missing header to lark_audio_skips_when_manager_none payload
- Remove redundant group-mention check in audio arm (shared check covers it)

---------

Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
2026-03-23 14:58:11 -04:00
Argenis
10f9ea3454
fix(security): update blocked_commands_basic test after #4338 (#4399)
* fix(security): update blocked_commands_basic test after allowing python/node

After adding python3 and node to default_allowed_commands in #4338,
the test asserting they are blocked is now wrong. Replace with ruby
and perl which remain correctly blocked.

* fix(security): also fix python3 reference in pipe validation test

The command_with_pipes_validates_all_segments test also referenced
python3 in a blocked assertion. Replace with ruby.
2026-03-23 14:16:42 -04:00
linyibin
3ec532bc29
fix(config): add cost tracking configuration section to template (#4368)
Add [cost] section to dev/config.template.toml with all configuration options and commented per-model pricing examples. Fixes #3679.
2026-03-23 14:14:59 -04:00
Nisit Sirimarnkit
f1688c5910
fix(agent): use Gregorian datetime and prioritize date context in prompts (#4369)
Force explicit Gregorian year/month/day via Datelike/Timelike traits instead of chrono format() which inherits system locale (e.g. Buddhist calendar on Thai systems). Prepend datetime before memory context in user messages. Rename DateTimeSection header to CRITICAL CONTEXT.
2026-03-23 14:14:56 -04:00
Argenis
fd9f140268
feat(tools): add cross-channel poll creation tool (#4396)
* feat(tools): add cross-channel poll creation tool

Adds a poll tool that enables cross-channel poll creation with voting
support. Changes all_tools_with_runtime return type from 3-tuple to
4-tuple to accommodate the new reaction handle.

Original PR #4243 by rareba.

* ci: retrigger CI
2026-03-23 13:58:01 -04:00
Argenis
b2dccf86eb
feat(tools): add Firecrawl fallback for JS-heavy sites (#4395)
* feat(tools): add Firecrawl fallback for JS-heavy and bot-blocked sites

Adds optional Firecrawl API integration as a fallback when standard web
fetching fails due to JavaScript-heavy pages or bot protection. Includes
configurable API key, timeout, and domain allowlist/blocklist.

Original PR #4244 by rareba.

* ci: retrigger CI
2026-03-23 13:57:56 -04:00
Argenis
f0c106a938
fix(update): diagnose arch mismatch in validate_binary before execution (#4379)
When a downloaded binary has the wrong architecture, the previous code
attempted to execute it and surfaced a raw "Exec format error (os error 8)".
Now validate_binary reads the ELF/Mach-O header first, compares the binary
architecture against the host, and reports a clear diagnostic like:
"architecture mismatch: downloaded binary is aarch64 but this host is x86_64".

Closes #4291
2026-03-23 13:36:44 -04:00
Keith_void
e276e66c05
fix(config): resolve macOS test failures from path canonicalization (#4362)
- Prefer HOME env var in default_config_dir() before falling back to UserDirs::new()
- Pre-create temp_default_dir in test so is_temp_directory() can canonicalize /var -> /private/var symlink on macOS
2026-03-23 13:33:19 -04:00
Giulio V
2300f21315
feat(channels): extend /new session reset to all channels (#4361)
Move supports_runtime_model_switch() guard from parse_runtime_command() entry to /model and /models match arms only, so /new is available on every channel.

Closes #4236
2026-03-23 13:33:06 -04:00
Giulio V
2575edb1d2
feat(tools): add LLM task tool for structured JSON-only sub-calls (#4241)
* feat(tools): add LLM task tool for structured JSON-only sub-calls

Add LlmTaskTool — a lightweight tool that runs a single prompt through
an LLM provider with no tool access and optionally validates the
response against a caller-supplied JSON Schema. Ideal for structured
data extraction, classification, and transformation in workflows.

- Parameters: prompt (required), schema (optional JSON Schema),
  model (optional override), temperature (optional override)
- Uses configured default provider/model from root config
- Validates response JSON against schema (required fields, type checks)
- Strips markdown code fences from LLM responses before validation
- Gated by ToolOperation::Act security policy
- Registered in all_tools_with_runtime (always available)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use non-constant value instead of approximate PI in tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: rareba <rareba@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:50:22 -04:00
Giulio V
d31f2c2d97
feat(agent): add loop detection guardrail for repetitive tool calls (#4240)
* feat(agent): add loop detection guardrail for repetitive tool calls

Introduces a LoopDetector that monitors a sliding window of recent tool
calls and detects three repetitive patterns:

1. Exact repeat — same tool+args called consecutively (default 3+)
2. Ping-pong — two tools alternating for 4+ cycles
3. No progress — same tool with different args but identical results (5+)

Each pattern escalates through Warning -> Block -> CircuitBreaker.
Configurable via [pacing] section: loop_detection_enabled (default true),
loop_detection_window_size (default 20), loop_detection_max_repeats
(default 3).

Wired into run_tool_call_loop alongside the existing time-gated
identical-output detector. Respects loop_ignore_tools exclusion list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(agent): fix channel test interaction with loop detector

The max_tool_iterations channel tests use an IterativeToolProvider that
intentionally repeats identical tool calls. The loop detector (enabled by
default) fires its circuit breaker before max_tool_iterations is reached,
causing the test to fail. Disable loop detection in these two tests so
they exercise only the max_tool_iterations boundary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(agent): address PR #4240 review — loop detector correctness and test precision

Critical fixes:
- Fix result_index/tool_calls misalignment: use enumerate() before
  filter_map() so the index stays aligned with tool_calls even when
  ordered_results contains None entries from skipped tool calls.
- Fix hash_value JSON key-order sensitivity: canonicalise() recursively
  sorts object keys before serialisation so {"a":1,"b":2} and
  {"b":2,"a":1} hash identically.

Tightened test assertions:
- ping_pong_escalates_with_more_cycles: assert Block with 5 cycles
  (was loose Warning|Block|Break match).
- no_progress_escalates_to_block_and_break: assert Break at 7 calls
  (was loose Block|Break match).
- no_progress_not_triggered_when_all_args_identical: assert Warning
  specifically (was accepting Ok as alternative).

New tests:
- ping_pong_detects_alternation_with_varying_args (item 3)
- window_eviction_prevents_stale_pattern_detection (item 4)
- hash_value_is_key_order_independent + nested variant (item 2)
- pacing_config_serde_defaults_match_manual_default (item 5)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: rareba <rareba@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:49:47 -04:00
Argenis
9d7f6c5aaf
fix(skills): surface actionable warning when skills are skipped due to script policy (#4383)
When a skill directory contains script files and `skills.allow_scripts`
is false (the default), the skill was silently skipped with only a
tracing::warn that most users never see. The LLM then reports "tool is
not available in this environment" with no guidance on how to fix it.

Now the loader emits a user-visible stderr warning that names the
skipped skill and suggests setting `skills.allow_scripts = true`.

Closes #4292
2026-03-23 12:47:32 -04:00
Argenis
f9081fcfa7
fix(update): use exact target triples in find_asset_url to prevent wrong binary selection (#4377)
The previous substring match (e.g. "aarch64-unknown-linux") could match
both gnu and android release assets. Switch to full triples like
"aarch64-unknown-linux-gnu" so find_asset_url only selects the correct
platform binary.

Closes #4293
2026-03-23 12:41:17 -04:00
Argenis
a4d95dec0e
fix(cost): enable cost tracking by default (#3679) (#4382)
Cost tracking was disabled by default (enabled: false), causing the
/api/cost endpoint to always return zeros unless users explicitly
opted in. Change the default to enabled: true so cost tracking works
out of the box for all models and channels.
2026-03-23 12:38:35 -04:00
Argenis
31508b8ec7
fix(build): disable prometheus on 32-bit ARM targets in installer (#3677) (#4384)
The prometheus crate requires AtomicU64 which is unavailable on 32-bit
ARM (armv7l/armv6l). Detect 32-bit ARM in install.sh and build with
--no-default-features, re-enabling only channel-nostr and
skill-creation (excluding observability-prometheus).
2026-03-23 12:35:09 -04:00
Argenis
3d9069552c
fix(gateway): send error responses for unrecognized WebSocket message types (#3681) (#4381)
Previously, the /ws/chat handler silently ignored messages with
unrecognized types, leaving clients waiting for a response that never
comes. Now sends explicit error messages describing the expected format.
2026-03-23 12:33:23 -04:00
Argenis
3b074041bf
fix(ollama): strip /api/chat suffix from user-provided base URL (#4376)
When users configure the Ollama api_url with the full endpoint path
(e.g. http://host:11434/api/chat), normalize_base_url only stripped
/api but not /api/chat, causing the final request URL to become
/api/chat/api/chat which fails on remote servers.

Closes #4342
2026-03-23 12:19:26 -04:00
Argenis
9a95318b85
fix(security): add python, python3, pip, node to default allowed commands (#4375)
The default_allowed_commands() list was missing common scripting
runtimes. After #3691 added serde defaults to AutonomyConfig, users
who omitted allowed_commands from their config silently fell back to
the restrictive default list, breaking shell tool access to Python.

Closes #4338
2026-03-23 12:19:23 -04:00
Argenis
ccd572b827
fix(matrix): handle unused Result from backups().disable() (#4374)
Add let _ = prefix to suppress unused Result warning on
client.encryption().backups().disable().await call.

Closes #4339
2026-03-23 12:19:10 -04:00
Argenis
41dd23175f
chore(ci): unify release pipeline for full auto-sync (#4283)
- Add tag-push trigger to Release Stable workflow so `git push origin v0.5.9`
  auto-triggers the full pipeline (builds, Docker, crates.io, website,
  Scoop, AUR, Homebrew, tweet) in one shot
- Add Homebrew Core as downstream job (was manual-only, never auto-triggered)
- Add `workflow_call` to pub-homebrew-core.yml so it can be called from
  the stable release workflow
- Skip beta releases on version bump commits (prevents beta/stable race)
- Skip auto crates.io publish when stable tag exists (prevents double-publish)
- Auto-create git tag on manual dispatch so tag always exists for downstream
- Fix cut_release_tag.sh to reference correct workflow name
2026-03-22 20:56:47 -04:00
Argenis
864d754b56
chore: bump version to 0.5.9 (#4282)
Release 0.5.9 includes:
- feat: enable internet access by default (#4270)
- feat: add browser automation skill and VNC setup scripts (#4281)
- feat: add voice message transcription support (#4278)
- feat: add image and file support for Feishu/Lark channel (#4280)
- feat: declarative cron job configuration (#4045)
- feat: add SearXNG search provider support (#4272)
- feat: register skill tools as callable tool specs (#4040)
- feat: named sessions with reconnect and validation (#4275)
- feat: restore time-decay scoring for memory (#4274)
- fix: prevent thinking level prefix leak across turns (#4277)
- fix: link enricher title extraction byte offset bug (#4271)
- fix: WhatsApp Web delivery channel with backend validation (#4273)
2026-03-22 20:23:55 -04:00
Argenis
ccd52f3394
feat: add browser automation skill and VNC setup scripts (#4281)
* feat: add browser automation skill and VNC setup scripts

- Add browser skill template for agent-browser CLI integration
- Add VNC setup scripts for GUI browser access (Xvfb, x11vnc, noVNC)
- Add comprehensive browser setup documentation
- Enables headless browser automation for AI agents

Tested on: Ubuntu 24.04, ZeroClaw 0.5.7, agent-browser 0.21.4

Co-authored-by: OpenClaw Assistant

* fix(docs): fix markdown lint errors and update browser config docs

- SKILL.md: add blank lines around headings (MD022)
- browser-setup.md: wrap bare URLs in angle brackets (MD034)
- browser-setup.md: rename duplicate "Access" heading (MD024)
- Update config examples to reflect browser enabled by default
- Add examples for restricting/disabling browser via config

---------

Co-authored-by: Argenis <theonlyhennygod@users.noreply.github.com>
2026-03-22 19:35:20 -04:00
Argenis
eb01aa451d
feat: add voice message transcription support (#4278)
Closes #4231. Adds voice memo detection and transcription for Slack and Discord channels. Audio files are downloaded, transcribed via the existing transcription module, and passed as text to the LLM.
2026-03-22 19:18:07 -04:00
Argenis
c785b45f2d
feat: add image and file support for Feishu/Lark channel (#4280)
Closes #4235. Adds image and file message type handling in the Lark channel - downloads images/files via Lark API, detects MIME types, and passes content to the model for analysis.
2026-03-22 19:14:43 -04:00
Argenis
ffb8b81f90
fix(agent): prevent thinking level prefix from leaking across turns (#4277)
* feat(agent): add thinking/reasoning level control per message

Users can set reasoning depth via /think:high etc. with resolution
hierarchy (inline > session > config > default). 6 levels from Off
to Max. Adjusts temperature and system prompt.

* fix(agent): prevent thinking level prefix from leaking across interactive turns

system_prompt was mutated in place for the first message's thinking
directive, then used as the "baseline" for restoration after each
interactive turn. This caused the first turn's thinking prefix to
persist across all subsequent turns.

Fix: save the original system_prompt before any thinking modifications
and restore from that saved copy between turns.
2026-03-22 19:09:12 -04:00
Argenis
65f856d710
fix(channels): link enricher title extraction byte offset bug (#4271)
* feat(channels): add automatic link understanding for inbound messages

Auto-detects URLs in inbound messages, fetches content, extracts
title+summary, and enriches the message before the agent sees it.
Includes SSRF protection (rejects private IPs).

* fix(channels): use lowercased string for title extraction to prevent byte offset mismatch

extract_title used byte offsets from the lowercased HTML to index into
the original HTML. Multi-byte characters whose lowercase form has a
different byte length (e.g. İ → i̇) would produce wrong slices or panics.
Fix: extract from the lowercased string directly. Add multibyte test.
2026-03-22 19:09:09 -04:00
Argenis
1682620377
feat(tools): enable internet access by default (#4270)
* feat(tools): enable internet access by default

Enable web_fetch, web_search, http_request, and browser tools by
default so ZeroClaw has internet access out of the box. Security
remains fully toggleable via config (set enabled = false to disable).

- web_fetch: enabled with allowed_domains = ["*"]
- web_search: enabled with DuckDuckGo (free, no API key)
- http_request: enabled with allowed_domains = ["*"]
- browser: enabled with allowed_domains = ["*"], agent_browser backend
- text_browser: remains opt-in (requires external binary)

* fix(tests): update component test for browser enabled by default

Update config_nested_optional_sections_default_when_absent to expect
browser.enabled = true, matching the new default.
2026-03-22 19:07:12 -04:00
Argenis
aa455ae89b
feat: declarative cron job configuration (#4045)
Add support for defining cron jobs directly in the TOML config file via
`[[cron.jobs]]` array entries. Declarative jobs are synced to the SQLite
database at scheduler startup with upsert semantics:

- New declarative jobs are inserted
- Existing declarative jobs are updated to match config
- Stale declarative jobs (removed from config) are deleted
- Imperative jobs (created via CLI/API) are never modified

Each declarative job requires a stable `id` for merge tracking. A new
`source` column (`"imperative"` or `"declarative"`) distinguishes the
two creation paths. Shell jobs require `command`, agent jobs require
`prompt`, validated before any DB writes.
2026-03-22 19:03:00 -04:00
Argenis
a9ffd38912
feat(memory): restore time-decay scoring lost in main→master migration (#4274)
Apply exponential time decay (2^(-age/half_life), 7-day half-life) to
memory entry scores post-recall. Core memories are exempt (evergreen).
Consolidate duplicate half-life constants into a single public constant
in the decay module.

Based on PR #4266 by 5queezer with constant consolidation fix.
2026-03-22 19:01:40 -04:00
Argenis
86a0584513
feat: add SearXNG search provider support (#4272)
Closes #4152. Adds SearXNG as a search provider option with JSON output support, configurable instance URL, env override support, and 7 new tests.
2026-03-22 19:01:35 -04:00
Argenis
abef4c5719
fix(cron): add WhatsApp Web delivery channel with backend validation (#4273)
Apply PR #4258 changes to add whatsapp/whatsapp-web/whatsapp_web match
arm in deliver_announcement, feature-gated behind whatsapp-web.

Added is_web_config() guard to bail early when the WhatsApp config is
for Cloud API mode (no session_path), preventing a confusing runtime
failure with an empty session path.
2026-03-22 18:58:26 -04:00
Argenis
483b2336c4
feat(gateway): add named sessions with reconnect and validation fixes (#4275)
* fix(cron): add WhatsApp Web delivery channel with backend validation

Apply PR #4258 changes to add whatsapp/whatsapp-web/whatsapp_web match
arm in deliver_announcement, feature-gated behind whatsapp-web.

Added is_web_config() guard to bail early when the WhatsApp config is
for Cloud API mode (no session_path), preventing a confusing runtime
failure with an empty session path.

* feat(gateway): add named sessions with human-readable labels

Apply PR #4267 changes with bug fixes:
- Add get_session_name trait method so WS session_start includes the
  stored name on reconnect (not just when ?name= query param is present)
- Rename API now returns 404 for non-existent sessions instead of
  silently succeeding
- Empty ?name= query param on WS connect no longer clears existing name
2026-03-22 18:58:15 -04:00
Argenis
14cda3bc9a
feat: register skill tools as callable tool specs (#4040)
Skill tools defined in [[tools]] sections are now registered as first-class
callable tool specs via the Tool trait, rather than only appearing as XML
in the system prompt. This enables the LLM to invoke skill tools through
native function calling.

- Add SkillShellTool for shell/script kind skill tools
- Add SkillHttpTool for http kind skill tools
- Add skills_to_tools() conversion and register_skill_tools() wiring
- Wire registration into both CLI and process_message agent paths
- Update prompt rendering to mark registered tools as callable
- Update affected tests across skills, agent/prompt, and channels
2026-03-22 18:51:24 -04:00
Argenis
6e8f0fa43c
docs: add ADR for tool shared state ownership contract (#4057)
Define the contract for long-lived shared state in multi-client tool
execution, covering ownership (handle pattern), identity assignment
(daemon-provided ClientId), lifecycle (validation at registration),
isolation (per-client for security state), and reload semantics
(config hash invalidation).
2026-03-22 18:40:34 -04:00
argenis de la rosa
a965b129f8 chore: bump version to 0.5.8
Release trigger bump after recent fixes.
2026-03-22 16:29:45 -04:00
Argenis
c135de41b7
feat(matrix): add allowed_rooms config for room-level gating (#4230) (#4260)
Add an `allowed_rooms` field to MatrixConfig that controls which rooms
the bot will accept messages from and join invites for. When the list
is non-empty, messages from unlisted rooms are silently dropped and
room invites are auto-rejected. When empty (default), all rooms are
allowed, preserving backward compatibility.

- Config: add `allowed_rooms: Vec<String>` with `#[serde(default)]`
- Message handler: replace disabled room_id filter with allowlist check
- Invite handler: auto-accept allowed rooms, auto-reject others
- Support both canonical room IDs and aliases, case-insensitive
2026-03-22 14:41:43 -04:00
Argenis
2d2c2ac9e6
feat(telegram): support forwarded messages with attribution (#4265)
Parse forward_from, forward_from_chat, and forward_sender_name fields
from Telegram message updates. Prepend forwarding attribution to message
content so the LLM has context about the original sender.

Closes #4118
2026-03-22 14:36:31 -04:00
Argenis
5e774bbd70
feat(multimodal): route image messages to dedicated vision provider (#4264)
When vision_provider is configured in [multimodal] config, messages
containing [IMAGE:] markers are automatically routed to the specified
vision-capable provider instead of failing on the default text provider.

Closes #4119
2026-03-22 14:36:29 -04:00
Argenis
33015067eb
feat(tts): add local Piper TTS provider (#4263)
Add a piper TTS provider that communicates with a local Piper/Coqui TTS
server via an OpenAI-compatible HTTP endpoint. This enables fully offline
voice pipelines: Whisper (STT) → LLM → Piper (TTS).

Closes #4116
2026-03-22 14:36:26 -04:00
Argenis
6b10c0b891
fix(approval): merge default auto_approve entries with user config (#4262)
When a user provides a custom `auto_approve` list in their TOML
config (e.g. to add an MCP tool), serde replaces the built-in
defaults instead of merging. This causes default safe tools like
`weather`, `calculator`, and `file_read` to lose auto-approve
status and get silently denied in non-interactive channel runs.

Add `ensure_default_auto_approve()` which merges built-in entries
into the user's list after deserialization, preserving user
additions while guaranteeing defaults are always present. Users
who want to require approval for a default tool can use
`always_ask`, which takes precedence.

Closes #4247
2026-03-22 14:28:09 -04:00
Argenis
bf817e30d2
fix(provider): prevent async runtime panic during model refresh (#4261)
Wrap `fetch_live_models_for_provider` calls in
`tokio::task::spawn_blocking` so the `reqwest::blocking::Client`
is created and dropped on a dedicated thread pool instead of
inside the async Tokio context. This prevents the
"Cannot drop a runtime in a context where blocking is not allowed"
panic when running `models refresh --provider openai`.

Closes #4253
2026-03-22 14:22:47 -04:00
Alix-007
0051a0c296
fix(matrix): enforce configured room scope on inbound events (#4251)
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
2026-03-22 14:08:13 -04:00
Canberk Özkan
d753de91f1
fix(skills): prevent panic by ensuring UTF-8 char boundary during truncation (#4252)
Fixed issue #4139.

Previously, slicing a string at exactly 64 bytes could land in the middle of a multi-byte UTF-8 character (e.g., Chinese characters), causing a runtime panic.

Changes:
- Replaced direct byte slicing with a safe boundary lookup using .char_indices().
- Ensures truncation always occurs at a valid character boundary at or before the 64-byte limit.
- Maintained existing hyphen-trimming logic.

Co-authored-by: loriscience <loriscience@gmail.com>
2026-03-22 14:08:01 -04:00
Argenis
f6b2f61a01
fix(matrix): disable automatic key backup when no backup key is configured (#4259)
Explicitly call `client.encryption().backups().disable()` when backups
are not enabled, preventing the matrix_sdk_crypto crate from attempting
room key backups on every sync cycle and spamming the logs with
"Trying to backup room keys but no backup key was found" warnings.

Closes #4227
2026-03-22 13:55:45 -04:00
Argenis
70e7910cb9
fix(web): remove unused import blocking release pipeline (#4234)
fix(web): remove unused import blocking release pipeline
2026-03-22 01:35:26 -04:00
argenis de la rosa
a8868768e8 fix(web): remove unused ChevronsUpDown import blocking release pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 01:20:51 -04:00
Argenis
67293c50df
chore: bump version to 0.5.7 (#4232)
chore: bump version to 0.5.7
2026-03-22 01:14:08 -04:00
argenis de la rosa
1646079d25 chore: bump version to 0.5.7
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 00:49:41 -04:00
Argenis
25b639435f
fix: merge voice-wake feature (PR #4162) with conflict resolution (#4225)
* feat(channels): add voice wake word detection channel

Add VoiceWakeChannel behind the `voice-wake` feature flag that:
- Captures audio from the default microphone via cpal
- Uses energy-based VAD to detect speech activity
- Transcribes speech via the existing transcription API (Whisper)
- Checks for a configurable wake word in the transcription
- On detection, captures the following utterance and dispatches it
  as a ChannelMessage

State machine: Listening -> Triggered -> Capturing -> Processing -> Listening

Config keys (under [channels_config.voice_wake]):
- wake_word (default: "hey zeroclaw")
- silence_timeout_ms (default: 2000)
- energy_threshold (default: 0.01)
- max_capture_secs (default: 30)

Includes tests for config parsing, state machine, RMS energy
computation, and WAV encoding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(config): fix pre-existing test compilation errors in schema.rs

- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): exclude voice-wake from all-features CI check

Add a `ci-all` meta-feature in Cargo.toml that includes every feature
except `voice-wake`, which requires `libasound2-dev` (ALSA) not present
on CI runners. Update the check-all-features CI job to use
`--features ci-all` instead of `--all-features`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:49:12 -04:00
Argenis
77779844e5
feat(memory): layered architecture upgrade + remove mem0 backend (#4226)
feat(memory): layered architecture upgrade + remove mem0 backend
2026-03-22 00:47:42 -04:00
Argenis
f658d5806a
fix: honor [autonomy] config section in daemon/channel mode
Fixes #4171
2026-03-22 00:47:32 -04:00
Argenis
7134fe0824
Merge pull request #4223 from zeroclaw-labs/fix/4214-heartbeat-utf8-safety
fix(heartbeat): prevent UTF-8 panic, add memory bounds and path validation
2026-03-22 00:41:47 -04:00
Argenis
263802b3df
Merge pull request #4224 from zeroclaw-labs/fix/4215-thai-i18n-cleanup
fix(i18n): remove extra keys and translate notion in th.toml
2026-03-22 00:41:21 -04:00
Argenis
3c25fddb2a
fix: merge Gmail Pub/Sub push PR #4164 (already integrated via #4200) (#4222)
* feat(channels): add Gmail Pub/Sub push notifications for real-time email

Add GmailPushChannel that replaces IMAP polling with Google's Pub/Sub
push notification system for real-time email-driven automation.

- New channel at src/channels/gmail_push.rs implementing the Channel trait
- Registers Gmail watch subscription (POST /gmail/v1/users/me/watch)
  with automatic renewal before the 7-day expiry
- Handles incoming Pub/Sub notifications at POST /webhook/gmail
- Fetches new messages via Gmail History API (startHistoryId-based)
- Dispatches email messages to the agent with full metadata
- Sends replies via Gmail messages.send API
- Config: gmail_push.enabled, topic, label_filter, oauth_token,
  allowed_senders, webhook_url
- OAuth token encrypted at rest via existing secret store
- Webhook endpoint added to gateway router
- 30+ unit tests covering notification parsing, header extraction,
  body decoding, sender allowlist, and config serialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(config): fix pre-existing test compilation errors in schema.rs

- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): fix extract_body_text_plain test

Gmail API sends base64url without padding. The decode_body function
converted URL-safe chars back to standard base64 but did not restore
the padding, causing STANDARD decoder to fail and falling back to
snippet. Add padding restoration before decoding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:40:42 -04:00
Argenis
a6a46bdd25
fix: add weather tool to default auto_approve list
Fixes #4170
2026-03-22 00:21:33 -04:00
Argenis
235d4d2f1c
fix: replace ILIKE substring matching with full-text search in postgres memory recall()
Fixes #4204
2026-03-22 00:20:11 -04:00
argenis de la rosa
bd1e8c8e1a merge: resolve conflicts with master + remove memory-mem0 from ci-all
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 00:18:09 -04:00
Argenis
f81807bff6
fix: serialize env-dependent codex tests to prevent race (#4210) (#4218)
Add a process-scoped Mutex that all env-var-mutating tests in
openai_codex::tests must hold.  This prevents std::env::set_var /
remove_var calls from racing when Rust's test harness runs them on
parallel threads.

Affected tests:
- resolve_responses_url_prefers_explicit_endpoint_env
- resolve_responses_url_uses_provider_api_url_override
- resolve_reasoning_effort_prefers_configured_override
- resolve_reasoning_effort_uses_legacy_env_when_unconfigured
2026-03-22 00:14:01 -04:00
argenis de la rosa
bb7006313c feat(memory): layered architecture upgrade + remove mem0 backend
Implement 6-phase memory system improvement:
- Multi-stage retrieval pipeline (cache → FTS → vector)
- Namespace isolation with strict filtering
- Importance scoring (category + keyword heuristics)
- Conflict resolution via Jaccard similarity + superseded_by
- Audit trail decorator (AuditedMemory<M>)
- Policy engine (quotas, read-only namespaces, retention rules)
- Deterministic sort tiebreaker on equal scores

Remove mem0 (OpenMemory) backend — all capabilities now covered
natively with better performance (local SQLite vs external REST API).

46 battle tests + 262 existing tests pass. Backward-compatible:
existing databases auto-migrate, existing configs work unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 00:09:43 -04:00
Argenis
9a49626376
fix: use POSIX-compatible sh -c instead of dash-specific -lc (#4209) (#4217)
* fix: build web dashboard during install.sh (#4207)

* fix: use POSIX-compatible sh -c instead of dash-specific -lc in cron scheduler (#4209)
2026-03-22 00:07:37 -04:00
Argenis
8b978a721f
fix: build web dashboard during install.sh (#4207) (#4216) 2026-03-22 00:02:54 -04:00
argenis de la rosa
75b4c1d4a4 fix(heartbeat): prevent UTF-8 panic, add memory bounds and path validation in session context
- Use char_indices for safe UTF-8 truncation instead of byte slicing
- Replace unbounded Vec with VecDeque rolling window in load_jsonl_messages
- Add path separator validation for channel/to to prevent directory traversal
2026-03-22 00:01:44 -04:00
argenis de la rosa
b2087e6065 fix(i18n): remove extra keys and translate untranslated notion entry in th.toml 2026-03-21 23:59:46 -04:00
Nisit Sirimarnkit
ad8f81ad76
Merge branch 'master' into i18n/thai-tool-descriptions 2026-03-22 10:28:47 +07:00
ninenox
c58e1c1fb3 i18n: add Thai tool descriptions 2026-03-22 10:09:03 +07:00
Martin Minkus
cb0779d761 feat(heartbeat): add load_session_context to inject conversation history
When `load_session_context = true` in `[heartbeat]`, the daemon loads the
last 20 messages from the target user's JSONL session file and prepends them
to the heartbeat task prompt before calling the LLM.

This gives the companion context — who the user is, what was last discussed —
so outreach messages feel like a natural continuation rather than a blank-slate
ping. Defaults to `false` (opt-in, no change to existing behaviour).

Key behaviours:
- Session context is re-read on every heartbeat tick (not cached at startup)
- Skips context injection if only assistant messages are present (prevents
  heartbeat outputs feeding back in a loop)
- Scans sessions directory for matching JSONL files using flexible filename
  matching: {channel}_{to}.jsonl, {channel}_*_{to}.jsonl, or
  {channel}_{to}_*.jsonl — handles varying session key formats
- Injects file mtime as "last message ~Xh ago" so the LLM knows how long
  the user has been silent

Config example:
  [heartbeat]
  enabled = true
  interval_minutes = 120
  load_session_context = true
  target = "telegram"
  to = "your_username"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 02:44:19 +00:00
Chris Hengge
daca2d9354
fix(web/tools): make section headings collapsible (#4180)
Agent Tools and CLI Tools section headings were static divs with no
way to collapse sections the user is not interested in, making the
page unwieldy with a large tool set.

- Convert both section heading divs to button elements toggling
  agentSectionOpen / cliSectionOpen state (both default open)
- Section content renders conditionally on those booleans
- ChevronsUpDown icon added (already in lucide-react bundle) that
  fades in on hover and indicates collapsed/expanded state
- No change to individual tool card parameter schema expand/collapse

Risk: Low — UI state only, no API or logic change.
Does not change: search/filter behaviour, tool card expand/collapse,
CLI tools table structure.
2026-03-21 22:25:18 -04:00
Chris Hengge
3c1e710c38
fix(web/logs): layout, footer status indicator, and empty-state note (#4203)
Three issues addressed:

Empty page: the logs page shows nothing at idle because the SSE stream
only carries ObserverEvent variants (llm_request, tool_call, error,
agent_start, agent_end). Daemon stdout and RUST_LOG tracing output go
to the terminal/log file and are never forwarded to the broadcast
channel — this is correct behaviour, not a misconfiguration. Added a
dismissible informational banner explaining what appears on the stream
and how to access tracing output (RUST_LOG + terminal).

Layout: flex-1 log entries div was missing min-h-0, which can cause
flex children to refuse to shrink below content size in some browsers.

Connection indicator: moved from the toolbar (where it cluttered the
title and controls) to a compact footer strip below the scroll area,
matching the /agent page pattern exactly.

Also added colour rules for llm_request, agent_start, agent_end,
tool_call_start event types which previously fell through to default
grey.

Risk: Low — UI layout and informational copy only, no backend change.
Does not change: SSE connection logic, event parsing, pause/resume,
type filters, or the underlying broadcast observer.
2026-03-21 22:11:20 -04:00
Chris Hengge
0aefde95f2
fix(web/config): fill viewport and add TOML syntax highlighting (#4201)
Two issues with the config editor:

Layout: the page root had no height constraint and the textarea used
min-h-[500px] resize-y, causing independent scrollbars on both the
page and the editor. Fixed by adopting the Memory/Cron flex column
pattern so the editor fills the remaining viewport height with a single
scroll surface.

Highlighting: plain textarea with no visual structure for TOML.
Added a zero-dependency layered pre-overlay technique — no new npm
packages (per CLAUDE.md anti-pattern rules). A pre element sits
absolute behind a transparent textarea; highlightToml() produces HTML
colour-coding sections, keys, strings, booleans, numbers, datetimes,
and comments via per-line regex. onScroll syncs the overlay. Tab key
inserts two spaces instead of leaving focus.

dangerouslySetInnerHTML used on the pre — content is the user's own
local config, not from the network, risk equivalent to any local editor.

Risk: Low-Medium — no API or backend change. New rendering logic
in editor only.
Does not change: save/load API calls, validation, sensitive field
masking behaviour.
2026-03-21 22:11:18 -04:00
Chris Hengge
a84aa60554
fix(web/cron): contain table scroll within viewport (#4186)
The cron page used a block-flow root with no height constraint, causing
the jobs table to grow taller than the viewport and the page itself to
scroll. This was inconsistent with the Memory page pattern.

- Change page root to flex flex-col h-full matching Memory's layout
- Table wrapper gains flex-1 min-h-0 overflow-auto so it fills
  remaining height and scrolls both axes internally
- Table header already has position:sticky so it pins correctly
  inside the scrolling container with no CSS change needed

Risk: Low — layout only, no logic or API change.
Does not change: job CRUD, modal, catch-up toggle, run history panel.
2026-03-21 22:11:15 -04:00
Chris Hengge
edd4b37325
fix(web/dashboard): rename channels card heading and add internal scroll (#4178)
The card heading used the key dashboard.active_channels ("Active Channels")
even though the card has a toggle between Active and All views, making the
static heading misleading. The channel list div had no height cap, causing
tall channel lists to stretch the card and break 3-column grid alignment.

- Change heading to t("dashboard.channels") — key already present in all
  three locales (zh/en/tr), no i18n changes needed
- Add overflow-y-auto max-h-48 pr-1 to the channel list wrapper so it
  scrolls internally instead of stretching the card
2026-03-21 22:09:00 -04:00
Argenis
c5f0155061
Merge pull request #4193 from zeroclaw-labs/fix/reaction-tool
fix(tools): pass platform channel_id to reaction trait
2026-03-21 21:38:32 -04:00
argenis de la rosa
9ee06ed6fc merge: resolve conflicts with master (image_gen + sessions) 2026-03-21 21:18:46 -04:00
Argenis
ac6b43e9f4
fix: remove unused channel_names field from DiscordHistoryChannel (#4199)
* feat: add discord history logging and search tool with persistent channel cache

* fix: remove unused channel_names field from DiscordHistoryChannel

The channel_names HashMap was declared and initialized but never used.
Channel name caching is handled via discord_memory.get()/store() with
the cache:channel_name: prefix. Remove the dead field.

* style: run cargo fmt on discord_history.rs

---------

Co-authored-by: ninenox <nisit15@hotmail.com>
2026-03-21 21:15:23 -04:00
Argenis
6c5573ad96
Merge pull request #4194 from zeroclaw-labs/fix/session-messaging-tools
fix(security): add enforcement and validation to session tools
2026-03-21 21:15:17 -04:00
Argenis
1d57a0d1e5
fix(web/tools): improve a11y in collapsible section headings (#4197)
* fix(web/tools): make section headings collapsible

Agent Tools and CLI Tools section headings were static divs with no
way to collapse sections the user is not interested in, making the
page unwieldy with a large tool set.

- Convert both section heading divs to button elements toggling
  agentSectionOpen / cliSectionOpen state (both default open)
- Section content renders conditionally on those booleans
- ChevronsUpDown icon added (already in lucide-react bundle) that
  fades in on hover and indicates collapsed/expanded state
- No change to individual tool card parameter schema expand/collapse

Risk: Low — UI state only, no API or logic change.
Does not change: search/filter behaviour, tool card expand/collapse,
CLI tools table structure.

* fix(web/tools): improve a11y and fix invalid HTML in collapsible sections

- Replace <h2> inside <button> with <span role="heading" aria-level={2}>
  to fix invalid HTML (heading elements not permitted in interactive content)
- Add aria-expanded attribute to section toggle buttons for screen readers
- Add aria-controls + id linking buttons to their controlled sections
- Replace ChevronsUpDown with ChevronDown icon — ChevronsUpDown is
  visually symmetric so rotating 180deg has no visible effect; ChevronDown
  rotating to -90deg gives a clear directional cue
- Remove unused ChevronsUpDown import

---------

Co-authored-by: WareWolf-MoonWall <chris.hengge@gmail.com>
2026-03-21 21:02:10 -04:00
Argenis
9780c7d797
fix: restrict free command to Linux-only in security policy (#4198)
* fix: resolve claude-code test flakiness and update security policy

* fix: restrict `free` command to Linux-only in security policy

`free` is not available on macOS or other BSDs. Move it behind
a #[cfg(target_os = "linux")] gate so it is only included in the
default allowed commands on Linux systems.

---------

Co-authored-by: ninenox <nisit15@hotmail.com>
2026-03-21 21:02:05 -04:00
Argenis
35a5451a17
fix(channels): address critical security bugs in Gmail Pub/Sub push (#4200)
* feat(channels): add Gmail Pub/Sub push notifications for real-time email

Add GmailPushChannel that replaces IMAP polling with Google's Pub/Sub
push notification system for real-time email-driven automation.

- New channel at src/channels/gmail_push.rs implementing the Channel trait
- Registers Gmail watch subscription (POST /gmail/v1/users/me/watch)
  with automatic renewal before the 7-day expiry
- Handles incoming Pub/Sub notifications at POST /webhook/gmail
- Fetches new messages via Gmail History API (startHistoryId-based)
- Dispatches email messages to the agent with full metadata
- Sends replies via Gmail messages.send API
- Config: gmail_push.enabled, topic, label_filter, oauth_token,
  allowed_senders, webhook_url
- OAuth token encrypted at rest via existing secret store
- Webhook endpoint added to gateway router
- 30+ unit tests covering notification parsing, header extraction,
  body decoding, sender allowlist, and config serialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(config): fix pre-existing test compilation errors in schema.rs

- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): fix extract_body_text_plain test

Gmail API sends base64url without padding. The decode_body function
converted URL-safe chars back to standard base64 but did not restore
the padding, causing STANDARD decoder to fail and falling back to
snippet. Add padding restoration before decoding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): address critical security bugs in Gmail Pub/Sub push

- Add webhook authentication via shared secret (webhook_secret config
  field or GMAIL_PUSH_WEBHOOK_SECRET env var), preventing unauthorized
  message injection through the unauthenticated webhook endpoint
- Add 1MB body size limit on webhook endpoint to prevent memory exhaustion
- Fix race condition in handle_notification: hold history_id lock across
  the read-fetch-update cycle to prevent duplicate message processing
  when concurrent webhook notifications arrive
- Sanitize RFC 2822 headers (To/Subject) to prevent CRLF injection
  attacks that could add arbitrary headers to outgoing emails
- Fix extract_email_from_header panic on malformed angle brackets by
  using rfind('>') and validating bracket ordering
- Add 30s default HTTP client timeout for all Gmail API calls,
  preventing indefinite hangs
- Clone tx sender before message processing loop to avoid holding
  the mutex lock across network calls

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 20:59:56 -04:00
Argenis
8e81d44d54
fix(gateway): address critical security and reliability bugs in Live Canvas (#4196)
* feat(gateway): add Live Canvas (A2UI) tool and real-time web viewer

Add a Live Canvas system that enables the agent to push rendered content
(HTML, SVG, Markdown, text) to a web-visible canvas in real time.

Backend:
- src/tools/canvas.rs: CanvasTool with render/snapshot/clear/eval actions,
  backed by a shared CanvasStore (Arc<RwLock<HashMap>>) with per-canvas
  broadcast channels for real-time updates
- src/gateway/canvas.rs: REST endpoints (GET/POST/DELETE /api/canvas/:id,
  GET /api/canvas/:id/history, GET /api/canvas) and WebSocket endpoint
  (WS /ws/canvas/:id) for real-time frame delivery

Frontend:
- web/src/pages/Canvas.tsx: Canvas viewer page with WebSocket connection,
  iframe sandbox rendering, canvas switcher, frame history panel

Registration:
- CanvasTool registered in all_tools_with_runtime (always available)
- Canvas routes wired into gateway router
- CanvasStore added to AppState
- Canvas page added to App.tsx router and Sidebar navigation
- i18n keys added for en/zh/tr locales

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(config): fix pre-existing test compilation errors in schema.rs

- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(gateway): share CanvasStore between tool and REST API

The CanvasTool and gateway AppState each created their own CanvasStore,
so content rendered via the tool never appeared in the REST API.

Create the CanvasStore once in the gateway, pass it to
all_tools_with_runtime via a new optional parameter, and reuse the
same instance in AppState.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(gateway): address critical security and reliability bugs in Live Canvas

- Validate content_type in REST POST endpoint against allowed set,
  preventing injection of "eval" frames via the REST API
- Enforce MAX_CONTENT_SIZE (256KB) limit on REST POST endpoint,
  matching tool-side validation to prevent memory exhaustion
- Add MAX_CANVAS_COUNT (100) limit to prevent unbounded canvas creation
  and memory exhaustion from CanvasStore
- Handle broadcast RecvError::Lagged in WebSocket handler gracefully
  instead of disconnecting the client
- Make MAX_CONTENT_SIZE and ALLOWED_CONTENT_TYPES pub for gateway reuse
- Update CanvasStore::render and subscribe to return Option for
  canvas count enforcement

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: rareba <rareba@users.noreply.github.com>
2026-03-21 20:59:18 -04:00
Argenis
86ad0c6a2b
fix(channels): address critical bugs in voice wake word detection (#4191)
* feat(channels): add voice wake word detection channel

Add VoiceWakeChannel behind the `voice-wake` feature flag that:
- Captures audio from the default microphone via cpal
- Uses energy-based VAD to detect speech activity
- Transcribes speech via the existing transcription API (Whisper)
- Checks for a configurable wake word in the transcription
- On detection, captures the following utterance and dispatches it
  as a ChannelMessage

State machine: Listening -> Triggered -> Capturing -> Processing -> Listening

Config keys (under [channels_config.voice_wake]):
- wake_word (default: "hey zeroclaw")
- silence_timeout_ms (default: 2000)
- energy_threshold (default: 0.01)
- max_capture_secs (default: 30)

Includes tests for config parsing, state machine, RMS energy
computation, and WAV encoding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(config): fix pre-existing test compilation errors in schema.rs

- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): exclude voice-wake from all-features CI check

Add a `ci-all` meta-feature in Cargo.toml that includes every feature
except `voice-wake`, which requires `libasound2-dev` (ALSA) not present
on CI runners. Update the check-all-features CI job to use
`--features ci-all` instead of `--all-features`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): address critical bugs in voice wake word detection

- Replace std::mem::forget(stream) with dedicated thread that holds the
  cpal stream and shuts down cleanly via oneshot channel, preventing
  microphone resource leaks on task cancellation
- Add config validation: energy_threshold must be positive+finite,
  silence_timeout_ms >= 100ms, max_capture_secs clamped to 300
- Guard WAV encoding against u32 overflow for large audio buffers
- Add hard cap on capture_buf size to prevent unbounded memory growth
- Increase audio channel buffer from 4 to 64 slots to reduce chunk
  drops during transcription API calls
- Remove dead WakeState::Processing variant that was never entered

---------

Co-authored-by: Giulio V <vannini.gv@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 20:43:19 -04:00
Argenis
6ecf89d6a9
fix(ci): skip release and publish workflows on forks (#4190)
When a fork syncs with upstream, GitHub attributes the push to the fork
owner, causing release-beta-on-push and publish-crates-auto to run
under the wrong identity — leading to confusing notifications and
guaranteed failures (missing secrets).

Add repository guards to root jobs so the entire pipeline is skipped
on forks.
2026-03-21 20:42:55 -04:00
argenis de la rosa
691efa4d8c style: fix cargo fmt formatting in reaction tool 2026-03-21 20:38:24 -04:00
argenis de la rosa
d1e3f435b4 style: fix cargo fmt formatting in session tools 2026-03-21 20:38:08 -04:00
Argenis
44c3e264ad
Merge pull request #4192 from zeroclaw-labs/fix/image-gen-tool
fix(tools): harden image_gen security and model validation
2026-03-21 20:37:27 -04:00
argenis de la rosa
f2b6013329 fix(tools): harden image_gen security enforcement and model validation
- Replace manual can_act()/record_action() with enforce_tool_operation()
  to match the codebase convention used by all other tools (notion,
  memory_forget, claude_code, delegate, etc.), producing consistent
  error messages and avoiding logic duplication.

- Add model parameter validation to prevent URL path traversal attacks
  via crafted model identifiers (e.g. "../../evil-endpoint").

- Add tests for model traversal rejection and filename sanitization.
2026-03-21 20:08:51 -04:00
argenis de la rosa
05d3c51a30 fix(security): add security policy enforcement and input validation to session tools
SessionsSendTool was missing security gate enforcement entirely - any agent
could send messages to any session without security policy checks. Similarly,
SessionsHistoryTool had no security enforcement for reading session data.

Changes:
- Add SecurityPolicy field to SessionsHistoryTool (enforces ToolOperation::Read)
- Add SecurityPolicy field to SessionsSendTool (enforces ToolOperation::Act)
- Add session_id validation to reject empty or non-alphanumeric-only IDs
- Pass security policy from all_tools_with_runtime registration
- Add tests for empty session_id, non-alphanumeric session_id validation
2026-03-21 20:04:44 -04:00
argenis de la rosa
2ceda31ce2 fix(tools): pass platform channel_id to reaction trait instead of channel name
The reaction tool was passing the channel adapter name (e.g. "discord",
"slack") as the first argument to Channel::add_reaction() and
Channel::remove_reaction(), but the trait signature expects a
platform-specific channel_id (e.g. Discord channel snowflake, Slack
channel ID like "C0123ABCD"). This would cause all reaction API calls
to fail at the platform level.

Fixes:
- Add required "channel_id" parameter to the tool schema
- Extract and pass channel_id (not channel_name) to trait methods
- Update tool description to mention the new parameter
- Add MockChannel channel_id capture for test verification
- Add test asserting channel_id (not name) reaches the trait
- Update all existing tests to supply channel_id
2026-03-21 20:01:22 -04:00
Argenis
9069bc3c1f
fix(agent): add system prompt budgeting for small-context models (#4185)
For models with small context windows (e.g. glm-4.5-air ~8K tokens),
the system prompt alone can exceed the limit. This adds:

- max_system_prompt_chars config option (default 0 = unlimited)
- compact_context now also compacts the system prompt: skips the
  Channel Capabilities section and shows only tool names
- Truncation with marker when prompt exceeds the budget

Users can set `max_system_prompt_chars = 8000` in [agent] config
to cap the system prompt for small-context models.

Closes #4124
2026-03-21 19:40:21 -04:00
Argenis
9319fe18da
fix(approval): support wildcard * in auto_approve and always_ask (#4184)
auto_approve = ["*"] was doing exact string matching, so only the
literal tool name "*" was matched. Users expecting wildcard semantics
had every tool blocked in supervised mode.

Also adds "prompt exceeds max length" to the context-window error
detection hints (fixes GLM/ZAI error 1261 detection).

Closes #4127
2026-03-21 19:38:11 -04:00
Argenis
cc454a86c8
fix(install): remove pairing code display from installer (#4176)
The gateway pairing code is now shown in the dashboard, so displaying
it in the installer output is redundant and cluttered (showed 3 codes).
2026-03-21 19:06:37 -04:00
Argenis
256e8ccebf
chore: bump version to v0.5.6 (#4174)
Update version across all distribution manifests:
- Cargo.toml / Cargo.lock
- dist/aur/PKGBUILD + .SRCINFO
- dist/scoop/zeroclaw.json
2026-03-21 18:03:38 -04:00
Argenis
72c9e6b6ca
fix(publish): publish aardvark-sys dep before main crate (#4172)
* fix(publish): add aardvark-sys version and publish it before main crate

- Add version = "0.1.0" to aardvark-sys path dependency in Cargo.toml
- Update all three publish workflows to publish aardvark-sys first
- Add aardvark-sys COPY to Dockerfile for workspace builds
- Fixes cargo publish failure: "dependency aardvark-sys does not
  specify a version"

* ci: publish aardvark-sys before main crate in all publish workflows

All three crates.io publish workflows now publish aardvark-sys first,
wait for indexing, then publish the main zeroclawlabs crate.
2026-03-21 16:20:50 -04:00
Argenis
755a129ca2
fix(install): use /dev/tty for sudo in curl|bash Xcode license accept (#4169)
When run via `curl | bash`, stdin is the curl pipe, so sudo cannot
prompt for a password. Redirect sudo's stdin from /dev/tty to reach
the real terminal, allowing the password prompt to work in piped
invocations.
2026-03-21 14:15:21 -04:00
Argenis
8b0d3684c5
fix(install): auto-accept Xcode license instead of bailing out (#4165)
Instead of exiting with a manual remediation step, the installer now
attempts to accept the Xcode/CLT license automatically via
`sudo xcodebuild -license accept`. Falls back to a clear error message
only if sudo fails (e.g. no terminal or password).
2026-03-21 13:57:38 -04:00
Giulio V
cdb5ac1471 fix(tools): fix remove_reaction_success test
The output format used "{action}ed" which produced "removeed" for the
remove action. Use explicit past-tense mapping instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:49:35 +01:00
Giulio V
67acb1a0bb fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:10:05 +01:00
Giulio V
9eac6bafef fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:09:48 +01:00
Giulio V
a12f2ff439 fix(config): fix pre-existing test compilation errors in schema.rs
- Remove #[cfg(unix)] gate on `use tempfile::TempDir` import since
  TempDir is used unconditionally in bootstrap file tests
- Add explicit type annotations on tokio::fs::* calls to resolve
  type inference failures (create_dir_all, write, read_to_string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:09:36 +01:00
Argenis
a38a4d132e
fix(hardware): drain stdin in subprocess test to prevent broken pipe flake (#4161)
* fix(hardware): drain stdin in subprocess test to prevent broken pipe flake

The test script did not consume stdin, so SubprocessTool's stdin write
raced against the process exit, causing intermittent EPIPE failures.
Add `cat > /dev/null` to drain stdin before producing output.

* style: format subprocess test
2026-03-21 12:19:53 -04:00
Argenis
48aba73d3a
fix(install): always check Xcode license on macOS, not just with --install-system-deps (#4153)
The Xcode license test-compile was inside install_system_deps(), which
only runs when --install-system-deps is passed. On macOS the default
path skipped this entirely, so users hit `cc` exit code 69 deep in
cargo build. Move the check into the unconditional main flow so it
always fires on Darwin.
2026-03-21 11:29:36 -04:00
Argenis
a1ab1e1a11
fix(install): use test-compile instead of xcrun for Xcode license detection (#4151)
xcrun --show-sdk-path can succeed even when the Xcode/CLT license has
not been accepted, so the previous check was ineffective. Replace it
with an actual test-compilation of a trivial C file, which reliably
triggers the exit-code-69 failure when the license is pending.
2026-03-21 11:03:07 -04:00
Giulio V
f394abf35c feat(tools): add standalone image generation tool via fal.ai
Add ImageGenTool that exposes fal.ai Flux model image generation as a
standalone tool, decoupled from the LinkedIn client. The tool accepts a
text prompt, optional filename/size/model parameters, calls the fal.ai
synchronous API, downloads the result, and saves to workspace/images/.

- New src/tools/image_gen.rs with full Tool trait implementation
- New ImageGenConfig in schema.rs (enabled, default_model, api_key_env)
- Config-gated registration in all_tools_with_runtime
- Security: checks can_act() and record_action() before execution
- Comprehensive unit tests (prompt validation, API key, size enum,
  autonomy blocking, tool spec)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:17:28 +01:00
Giulio V
52e0271bd5 feat(tools): add emoji reaction tool for cross-channel reactions
Add ReactionTool that exposes Channel::add_reaction and
Channel::remove_reaction as an agent-callable tool. Uses a
late-binding ChannelMapHandle (Arc<RwLock<HashMap>>) pattern
so the tool can be constructed during tool registry init and
populated once channels are available in start_channels.

Parameters: channel, message_id, emoji, action (add/remove).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:15:25 +01:00
Giulio V
6c0a48efff feat(tools): add session list, history, and send tools for inter-agent messaging
Add three new tools in src/tools/sessions.rs:
- sessions_list: lists active sessions with channel, message count, last activity
- sessions_history: reads last N messages from a session by ID
- sessions_send: appends a message to a session for inter-agent communication

All tools operate on the SessionBackend trait, using the JSONL SessionStore
by default. Registered unconditionally in all_tools_with_runtime when the
sessions directory is accessible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:07:18 +01:00
SimianAstronaut7
87b5bca449
feat(config): add configurable pacing controls for slow/local LLM workloads (#3343)
* feat(config): add configurable pacing controls for slow/local LLM workloads (#2963)

Add a new `[pacing]` config section with four opt-in parameters that
let users tune timeout and loop-detection behavior for local LLMs
(Ollama, llama.cpp, vLLM) without disabling safety features entirely:

- `step_timeout_secs`: per-step LLM inference timeout independent of
  the overall message budget, catching hung model responses early.
- `loop_detection_min_elapsed_secs`: time-gated loop detection that
  only activates after a configurable grace period, avoiding false
  positives on long-running browser/research workflows.
- `loop_ignore_tools`: per-tool loop-detection exclusions so tools
  like `browser_screenshot` that structurally resemble loops are not
  counted toward identical-output detection.
- `message_timeout_scale_max`: overrides the hardcoded 4x ceiling in
  the channel message timeout scaling formula.

All parameters are strictly optional with no effect when absent,
preserving full backwards compatibility.

Closes #2963

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(config): add missing pacing fields in tests and call sites

* fix(config): add pacing arg to remaining cost-tracking test call sites

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
2026-03-21 08:54:08 -04:00
Argenis
be40c0c5a5
Merge pull request #4145 from zeroclaw-labs/feat/gateway-path-prefix
feat(gateway): add path_prefix for reverse-proxy deployments
2026-03-21 08:48:56 -04:00
argenis de la rosa
6527871928 fix: add path_prefix to test AppState in gateway/api.rs 2026-03-21 08:14:28 -04:00
argenis de la rosa
0bda80de9c feat(gateway): add path_prefix for reverse-proxy deployments
Adopted from #3709 by @slayer with minor cleanup.
Supersedes #3709
2026-03-21 08:14:28 -04:00
Argenis
02f57f4d98
Merge pull request #4144 from zeroclaw-labs/feat/claude-code-tool
feat(tools): add ClaudeCodeTool for two-tier agent delegation
2026-03-21 08:14:19 -04:00
Argenis
ef83dd44d7
Merge pull request #4146 from zeroclaw-labs/feat/memory-recall-time-range
feat(memory): add time range filter to recall (since/until)
2026-03-21 08:14:12 -04:00
Argenis
a986b6b912
fix(install): detect un-accepted Xcode license + bump to v0.5.5 (#4147)
* fix(install): detect un-accepted Xcode license before build

Add an xcrun check after verifying Xcode CLT is installed. When the
Xcode/CLT license has not been accepted, cc exits with code 69 and
the build fails with a cryptic linker error. This surfaces a clear
message telling the user to run `sudo xcodebuild -license accept`.

* chore(release): bump version to v0.5.5

Update version across all distribution manifests:
- Cargo.toml and Cargo.lock
- dist/aur/PKGBUILD and .SRCINFO
- dist/scoop/zeroclaw.json
2026-03-21 08:09:27 -04:00
SimianAstronaut7
b6b1186e3b
feat(channel): add per-channel proxy_url support for HTTP/SOCKS5 proxies (#3345)
* feat(channel): add per-channel proxy_url support for HTTP/SOCKS5 proxies

Allow each channel to optionally specify a `proxy_url` in its config,
enabling users behind restrictive networks to route channel traffic
through HTTP or SOCKS5 proxies. When set, the per-channel proxy takes
precedence over the global `[proxy]` config; when absent, the channel
falls back to the existing runtime proxy behavior.

Adds `proxy_url: Option<String>` to all 12 channel config structs
(Telegram, Discord, Slack, Mattermost, Signal, WhatsApp, Wati,
NextcloudTalk, DingTalk, QQ, Lark, Feishu) and introduces
`build_channel_proxy_client`, `build_channel_proxy_client_with_timeouts`,
and `apply_channel_proxy_to_builder` helpers that normalize proxy URLs
and integrate with the existing client cache.

Closes #3262

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(channel): add missing proxy_url fields in test initializers

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
2026-03-21 07:53:20 -04:00
SimianAstronaut7
00dc0c8670
feat(tool): enrich delegate sub-agent system prompt and add skills_directory config key (#3344)
* feat(tool): enrich delegate sub-agent system prompt and add skills_directory config key (#3046)

Sub-agents configured under [agents.<name>] previously received only the
bare system_prompt string. They now receive a structured system prompt
containing: tools section (allowed tools with parameters and invocation
protocol), skills section (from scoped or default directory), workspace
path, current date/time, safety constraints, and shell policy when shell
is in the effective tool list.

Add optional skills_directory field to DelegateAgentConfig for per-agent
scoped skill loading. When unset, falls back to default workspace
skills/ directory.

Closes #3046

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tools): add missing fields after rebase

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
2026-03-21 07:53:02 -04:00
argenis de la rosa
43f2a0a815 fix: add ClaudeCodeConfig to config re-exports and fix formatting 2026-03-21 07:51:36 -04:00
argenis de la rosa
50b5bd4d73 ci: retrigger CI after stuck runners 2026-03-21 07:46:34 -04:00
argenis de la rosa
8c074870a1 fix(memory): replace redundant closures with function references
Clippy flagged `.map(|s| chrono::DateTime::parse_from_rfc3339(s))` as
redundant — use `.map(chrono::DateTime::parse_from_rfc3339)` directly.
2026-03-21 07:46:34 -04:00
argenis de la rosa
61d1841ce3 fix: update gateway mock Memory impls with since/until params
Both test mock implementations of Memory::recall() in gateway/mod.rs
were missing the new since/until parameters.
2026-03-21 07:46:34 -04:00
argenis de la rosa
eb396cf38f feat(memory): add time range filter to recall (since/until)
Adopted from #3705 by @fangxueshun with fixes:
- Added input validation for date strings (RFC 3339)
- Used chrono DateTime comparison instead of string comparison
- Added since < until validation
- Updated mem0 backend
Supersedes #3705
2026-03-21 07:46:34 -04:00
argenis de la rosa
9f1657b9be fix(tools): use kill_on_drop for ClaudeCodeTool subprocess timeout 2026-03-21 07:46:24 -04:00
argenis de la rosa
8fecd4286c fix(tools): use kill_on_drop for ClaudeCodeTool subprocess timeout
Fixes E0382 borrow-after-move error: wait_with_output() consumed the
child handle, making child.kill() in the timeout branch invalid.
Use kill_on_drop(true) with cmd.output() instead.
2026-03-21 07:46:24 -04:00
argenis de la rosa
df21d92da3 feat(tools): add ClaudeCodeTool for two-tier agent delegation
Adopted from #3748 by @ilyasubkhankulov with fixes:
- Removed unused _runtime field
- Fixed subprocess timeout handling
- Excluded unrelated Slack threading and Dockerfile changes

Closes #3748 (superseded)
2026-03-21 07:46:24 -04:00
Argenis
8d65924704
fix(channels): add cost tracking and enforcement to all channels (#4143)
Adds per-channel cost tracking via task-local context in the tool call
loop. Budget enforcement blocks further API calls when limits are
exceeded. Resolves merge conflicts with model-switch retry loop,
reply_target parameter, and autonomy level additions on master.

Supersedes #3758
2026-03-21 07:37:15 -04:00
Argenis
756c3cadff
feat(transcription): add LocalWhisperProvider for self-hosted STT (TDD) (#4141)
Self-hosted Whisper-compatible STT provider that POSTs audio to a
configurable HTTP endpoint (e.g. faster-whisper over WireGuard). Audio
never leaves the platform perimeter.

Implemented via red/green TDD cycles:
  Wave 1 — config schema: LocalWhisperConfig struct, local_whisper field
    on TranscriptionConfig + Default impl, re-export in config/mod.rs
  Wave 2 — from_config validation: url non-empty, url parseable, bearer_token
    non-empty, max_audio_bytes > 0, timeout_secs > 0; returns Result<Self>
  Wave 3 — manager integration: registration with ? propagation (not if let Ok
    — credentials come directly from config, no env-var fallback; present
    section with bad values is a hard error, not a silent skip)
  Wave 4 — transcribe(): resolve_audio_format() extracted from validate_audio()
    so LocalWhisperProvider can resolve MIME without the 25 MB cloud cap;
    size check + format resolution before HTTP send
  Wave 5 — HTTP mock tests: success response, bearer auth header, 503 error

33 tests (20 baseline + 13 new), all passing. Clippy clean.

Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
2026-03-21 07:15:36 -04:00
Argenis
ee870028ff
feat(channel): use Slack native markdown blocks for rich formatting (#4142)
Slack's Block Kit supports a native `markdown` block type that accepts
standard Markdown and handles rendering. This removes the need for a
custom Markdown-to-mrkdwn converter. Messages over 12,000 chars fall
back to plain text.

Co-authored-by: Joe Hoyle <joehoyle@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 07:12:27 -04:00
frido22
83183a39a5
feat(status): show service running state in zeroclaw status (#3751) 2026-03-21 06:49:47 -04:00
shiben
7a941fb753
feat(auth): add import functionality for existing OpenAI Codex auth p… (#3762)
* feat(auth): add import functionality for existing OpenAI Codex auth profiles

Introduces a new command-line option to import an existing `auth.json` file for OpenAI Codex, allowing users to bypass the login flow. The import feature reads and parses the specified JSON file, extracting authentication tokens and storing them in the user's profile. This change enhances user experience by simplifying the authentication process for existing users.

- Added `import` option to `AuthCommands` enum
- Implemented `import_openai_codex_auth_profile` function to handle the import logic
- Updated `handle_auth_command` to process the import option and validate provider compatibility
- Ensured that the import feature is exclusive to the `openai-codex` provider

* feat(auth): extract expiry from JWT in OpenAI Codex import

Enhances the `import_openai_codex_auth_profile` function by extracting the expiration date from the JWT access token. This change allows for more accurate management of token lifetimes by replacing the hardcoded expiration date with a dynamic value derived from the token itself.

- Added `extract_expiry_from_jwt` function to handle JWT expiration extraction
- Updated `TokenSet` to use the extracted expiration date instead of a static value
2026-03-21 06:49:44 -04:00
Argenis
bcdbce0bee
feat(web): add theme system with CSS variables and settings modal (#4133)
- Add ThemeContext with light/dark/system theme support
- Migrate all hardcoded colors to CSS variables
- Add SettingsModal for theme customization
- Add font loader for dynamic font selection
- Add i18n support for Chinese and Turkish locales
- Fix accessibility: add aria-live to pairing error message

Co-authored-by: nanyuantingfeng <nanyuantingfeng@163.com>
2026-03-21 06:22:30 -04:00
Argenis
abb844d7f8
fix(config): add missing WhatsApp Web policy config keys (#4131)
* fix(config): add missing WhatsApp Web policy config keys (mode, dm_policy, group_policy, self_chat_mode)

* fix(onboard): add missing WhatsApp policy fields to wizard struct literals

The new mode, dm_policy, group_policy, and self_chat_mode fields added
to WhatsAppConfig need default values in the onboard wizard's struct
initializers to avoid E0063 compilation errors.
2026-03-21 06:04:21 -04:00
Argenis
48733d5ee2
feat(cron): add Edit button and modal for updating cron jobs (#4132)
- Backend: add PATCH /api/cron/{id} handler (handle_api_cron_patch)
  using update_shell_job_with_approval with approved=false; validates
  job exists (404 on miss), accepts name/schedule/command patch fields
- Router: register PATCH on /api/cron/{id} alongside existing DELETE
- Frontend API: add patchCronJob(id, patch) calling PATCH /api/cron/{id}
- i18n: add cron.edit, cron.edit_modal_title, cron.edit_error,
  cron.saving, cron.save keys to all 3 locales (zh, en, tr)
- UI: Edit (Pencil) button in Actions column opens a pre-populated modal
  with the job's current name, schedule expression, and command;
  submitting PATCHes the job and updates the table row in-place

Co-authored-by: WareWolf-MoonWall <chris.hengge@gmail.com>
2026-03-21 05:50:23 -04:00
Argenis
2d118af78f
fix(channels): wire model_switch callback into channel inference path (#4130)
The channel path in `src/channels/mod.rs` was passing `None` as the
`model_switch_callback` to `run_tool_call_loop()`, which meant model
switching via the `model_switch` tool was silently ignored in channel
mode.

Wire the callback in following the same pattern as the CLI path:
- Pass `Some(model_switch_callback.clone())` instead of `None`
- Wrap the tool call loop in a retry loop
- Handle `ModelSwitchRequested` errors by re-creating the provider
  with the new model and retrying

Fixes #4107
2026-03-21 05:43:21 -04:00
Argenis
8d7e7e994e
fix(memory): use plain OS threads for postgres operations to avoid nested runtime panic (#4129)
Replace `tokio::task::spawn_blocking()` with plain `std:🧵:Builder`
OS threads in all PostgresMemory trait methods. The sync `postgres` crate
(v0.19.x) internally calls `Runtime::block_on()`, which panics when called
from Tokio's blocking pool threads in recent Tokio versions. Plain OS threads
have no runtime context, so the nested `block_on` succeeds.

This matches the pattern already used in `PostgresMemory::initialize_client()`,
which correctly used `std:🧵:Builder` and never exhibited this bug.

A new `run_on_os_thread` helper centralizes the pattern: spawn an OS thread,
run the closure, and bridge the result back via a `tokio::sync::oneshot` channel.

Fixes #4101
2026-03-21 05:33:55 -04:00
Joe Hoyle
d38d706c8e
feat(channel): add Slack Assistants API status indicators (#4105)
Implement start_typing/stop_typing for Slack using the Assistants API
assistant.threads.setStatus method. Tracks thread context from
assistant_thread_started events and inbound messages, then sets
"is thinking..." status during processing. Status auto-clears when
the bot sends a reply via chat.postMessage.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 05:32:31 -04:00
Chris Hengge
523188da08
feat(tools): add WeatherTool with wttr.in integration (#4104)
Implements the Weather integration as a native Rust Tool trait
implementation, consistent with the existing first-party tool
architecture (no WASM/plugin layer required).

- Add src/tools/weather_tool.rs with full WeatherTool impl
  - Fetches from wttr.in ?format=j1 (no API key, global coverage)
  - Supports city names (any language/script), IATA airport codes,
    GPS coordinates, postal/zip codes, domain-based geolocation
  - Metric (°C, km/h, mm) and imperial (°F, mph, in) units
  - Current conditions + 0-3 day forecast with hourly breakdown
  - Graceful error messages for unknown/invalid locations
  - Respects runtime proxy config via apply_runtime_proxy_to_builder
  - 36 unit tests: schema, URL building, param validation, formatting
- Register WeatherTool unconditionally in all_tools_with_runtime
  (no API key needed, no config gate — same pattern as CalculatorTool)
- Flip integrations registry Weather entry from ComingSoon to Available

Closes #<issue>
2026-03-21 05:32:28 -04:00
Baha Abu Nojaim
82f7fbbe0f
feat(providers): add DeepMyst as OpenAI-compatible provider (#4103)
Register DeepMyst (https://deepmyst.com) as an OpenAI-compatible
provider with Bearer auth and DEEPMYST_API_KEY env var support.
Aliases: "deepmyst", "deep-myst".

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 05:32:26 -04:00
Caleb
c1b2fceca5
fix(onboard): make tmux paste safe for text prompts (#4106) 2026-03-21 05:14:37 -04:00
Loc Nguyen Huu
be6e9fca5d
fix(docker): align workspace with Cargo.lock for --locked builds (#4126)
The builder used sed to remove crates/robot-kit from [workspace].members because that path was not copied into the image. Cargo.lock is still generated for the full workspace (including zeroclaw-robot-kit), so the manifest and lockfile disagreed. cargo build --release --locked then tried to rewrite Cargo.lock and failed with "cannot update the lock file ... because --locked was passed" (commonly hit when ZEROCLAW_CARGO_FEATURES includes memory-postgres).

Copy crates/robot-kit/ into the image and drop the sed step so the workspace matches the committed lockfile.

Made-with: Cursor

Co-authored-by: lokinh <locnh@uniultra.xyz>
2026-03-21 05:14:35 -04:00
Greg Lamberson
75c11dfb92
fix(config): prevent test suite from clobbering active_workspace.toml (#4121)
* fix(config): prevent test suite from clobbering active_workspace.toml

Refactor persist_active_workspace_config_dir() to accept the default
config directory as an explicit parameter instead of reading HOME
internally. This eliminates a hidden dependency on process-wide
environment state that caused test-suite runs to overwrite the real
user's active_workspace.toml with a stale temp-directory path.

The temp-directory guard is now unconditional (previously gated behind
cfg(not(test))). It rejects writes only when a temp config_dir targets
a non-temp default location, so test-to-test writes within temp dirs
still succeed.

Closes #4117

* fix: remove needless borrow on default_config_dir parameter

---------

Co-authored-by: lamco-office <office@lamco.io>
2026-03-21 05:14:32 -04:00
tf4fun
48270fbbf3
fix(cron): add qq to supported delivery channel whitelist (#4120)
The channel validation in `validate_announce_delivery` was missing `qq`,
causing API-created cron jobs with QQ delivery to be rejected.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 05:14:30 -04:00
Argenis
18a456b24e
fix(mcp): wire MCP tools into WebSocket chat and gateway /api/tools (#4096)
* fix(mcp): wire MCP tools into WebSocket chat path and gateway /api/tools

Agent::from_config() did not initialize MCP tools because it was
synchronous and MCP connection requires async. The gateway tool
registry built for /api/tools also missed MCP tools for the same
reason.

Changes:
- Make Agent::from_config() async so it can call McpRegistry::connect_all()
- Add MCP tool initialization (both eager and deferred modes) to
  from_config(), following the same pattern used in loop_.rs CLI/webhook paths
- Add MCP tool initialization to the gateway's tool registry so
  /api/tools reflects MCP tools
- Update all three call sites (run(), handle_socket, test) to await

Closes #4042

* fix: merge master and fix formatting

* fix: remove underscore prefix from used bindings (clippy)
2026-03-21 05:13:01 -04:00
ehu shubham shaw
71e89801b5
feat(hardware): add RPi GPIO, Aardvark I2C/SPI/GPIO, and hardware plugin system (#4125)
* feat(hardware): add RPi GPIO, Aardvark I2C/SPI/GPIO, and hardware plugin system

Extends the hardware subsystem with three clusters of functionality,
all feature-gated (hardware / peripheral-rpi) with no impact on default builds.

Raspberry Pi native support:
- src/hardware/rpi.rs: board self-discovery (model, serial, revision),
  sysfs GPIO pin read/write, and ACT LED control
- scripts/99-act-led.rules: udev rule for non-root ACT LED access
- scripts/deploy-rpi.sh, scripts/rpi-config.toml, scripts/zeroclaw.service:
  one-shot deployment helper and systemd service template

Total Phase Aardvark USB adapter (I2C / SPI / GPIO):
- crates/aardvark-sys/: new workspace crate with FFI bindings loaded at
  runtime via libloading; graceful stub fallback when .so is absent or
  arch mismatches (Rosetta 2 detection)
- src/hardware/aardvark.rs: AardvarkTransport implementing Transport trait
- src/hardware/aardvark_tools.rs: agent tools i2c_scan, i2c_read,
  i2c_write, spi_transfer, gpio_aardvark
- src/hardware/datasheet.rs: datasheet search/download for detected devices
- docs/aardvark-integration.md, examples/hardware/aardvark/: guide + examples

Hardware plugin / ToolRegistry system:
- src/hardware/tool_registry.rs: ToolRegistry for hardware module tool sets
- src/hardware/loader.rs, src/hardware/manifest.rs: manifest-driven loader
- src/hardware/subprocess.rs: subprocess execution helper for board I/O
- src/gateway/hardware_context.rs: POST /api/hardware/reload endpoint
- src/hardware/mod.rs: exports all new modules; merge_hardware_tools and
  load_hardware_context_prompt helpers

Integration hooks (minimal surface):
- src/hardware/device.rs: DeviceKind::Aardvark, DeviceRuntime::Aardvark,
  has_aardvark / resolve_aardvark_device on DeviceRegistry
- src/hardware/transport.rs: TransportKind::Aardvark
- src/peripherals/mod.rs: gate create_board_info_tools behind hardware feature
- src/agent/loop_.rs: TOOL_CHOICE_OVERRIDE task-local for Anthropic provider
- src/providers/anthropic.rs: read TOOL_CHOICE_OVERRIDE; add tool_choice field
- Cargo.toml: add aardvark-sys to workspace and as dependency
- firmware/zeroclaw-nucleo/: update Cargo.toml and Cargo.lock

Non-goals:
- No changes to agent orchestration, channels, providers, or security policy
- No new config keys outside existing [hardware] / [peripherals] sections
- No CI workflow changes

Risk: Low. All new paths are feature-gated; aardvark.so loads at runtime
only when present. No schema migrations or persistent state introduced.

Rollback: revert this single commit.

* fix(hardware): resolve clippy and rustfmt CI failures

- struct_excessive_bools: allow on DeviceCapabilities (7 bool fields needed)
- unnecessary_debug_formatting: use .display() instead of {:?} for paths
- stable_sort_primitive: replace .sort() with .sort_unstable() on &str slices

* fix(hardware): add missing serial/uf2/pico modules declared in mod.rs

cargo fmt was exiting with code 1 because mod.rs declared pub mod serial,
uf2, pico_flash, pico_code but those files were missing from the branch.
Also apply auto-formatting to loader.rs.

* fix(hardware): apply rustfmt 1.92.0 formatting (matches CI toolchain)

* docs(scripts): add RPi deployment and interaction guide

* push

* feat(firmware): add initial Pico firmware and serial device handling

- Introduced main.py for ZeroClaw Pico firmware with a placeholder for MicroPython implementation.
- Added binary UF2 file for Pico deployment.
- Implemented serial device enumeration and validation in the hardware module, enhancing security by restricting allowed serial paths.
- Updated related modules to integrate new serial device functionality.

---------

Co-authored-by: ehushubhamshaw <eshaw1@wpi.edu>
2026-03-21 04:17:01 -04:00
Argenis
46f6e79557
fix(gateway): improve error message for Docker bridge connectivity (#4095)
When the gateway security guard blocks a public bind address, the error
message now mentions the Docker use case and provides clear instructions
for connecting from Docker containers.

Closes #4086
2026-03-21 00:16:01 -04:00
Argenis
c301b1d4d0
fix(approval): auto-approve read-only tools in non-interactive mode (#4094)
* fix(approval): auto-approve read-only tools in non-interactive mode

Add web_search_tool, web_fetch, calculator, glob_search, content_search,
and image_info to the default auto_approve list. These are read-only tools
with no side effects that were being silently denied in channel mode
(Telegram, Slack, etc.) because the non-interactive ApprovalManager
auto-denies any tool not in auto_approve when autonomy != full.

Closes #4083

* fix: remove duplicate default_otp_challenge_max_attempts function
2026-03-20 23:57:43 -04:00
Argenis
981a93d942
fix(config): remove duplicate default_otp_challenge_max_attempts function (#4098)
The function was defined twice in schema.rs after merging #3921, causing
compilation failures on all downstream branches.
2026-03-20 23:57:27 -04:00
Argenis
34f0b38e42
fix(config): remove duplicate default_otp_challenge_max_attempts function (#4097)
PR #3921 accidentally introduced a duplicate definition of
default_otp_challenge_max_attempts() in config/schema.rs, causing
compilation to fail on master (E0428: name defined multiple times).
2026-03-20 18:49:15 -04:00
khhjoe
00209dd899
feat(memory): add mem0 (OpenMemory) backend integration (#3965)
* feat(memory): add mem0 (OpenMemory) backend integration

- Implement Mem0Memory struct with full Memory trait
- Add history() audit trail, recall_filtered() with time/metadata filters
- Add store_procedural() for conversation trace extraction
- Add ProceduralMessage type to Memory trait with default no-op
- Feature-gated behind `memory-mem0` flag
- 9 unit tests covering edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: apply cargo fmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(memory): add extraction_prompt config, deploy scripts, and timing instrumentation

- Add `extraction_prompt` field to `Mem0Config` for custom LLM fact
  extraction prompts (e.g. Cantonese/Chinese content), with
  `MEM0_EXTRACTION_PROMPT` env var fallback
- Pass `custom_instructions` in mem0 store requests so the server
  uses the client-supplied prompt over its default
- Add timing instrumentation to channel message pipeline
  (mem_recall_ms, elapsed_before_llm_ms, llm_call_ms, total_ms)
- Add `deploy/mem0/` with self-hosted mem0 + reranker GPU server
  scripts, fully configurable via environment variables
- Update config reference docs (EN, zh-CN, VI) with `[memory.mem0]`
  subsection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

# Conflicts:
#	src/channels/mod.rs

* chore: remove accidentally staged worktree from index

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 18:22:44 -04:00
caoy
9e8a478254
fix(gemini): use default chat() for prompt-guided tool calling and add vision support (#3932)
* fix(gemini): use default chat() for prompt-guided tool calling and add multimodal vision support

The Gemini provider's chat() override bypassed the default trait
implementation that injects tool definitions into the system prompt for
providers without native tool calling. This caused the agent loop to see
tool definitions in context but never actually invoke them, resulting in
hallucinated tool calls (e.g. claiming "Stored" without calling
memory_store).

Remove the broken chat() override so the default prompt-guided fallback
in the Provider trait handles tool injection correctly. Add an explicit
capabilities() declaration (native_tool_calling: false, vision: true).

Also add multimodal support: convert Part from a plain struct to an
untagged enum with Text and Inline variants, and add build_parts() to
extract [IMAGE:data:...] markers as Gemini inline_data parts.

Includes 14 new tests covering capabilities, Part serialization,
build_parts edge cases, and role-mapping behavior. Removes unused
ChatResponse import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(gemini): move capabilities tests to component level and add tool conversion test

Move the 3 capabilities tests (native_tool_calling, vision,
supports_native_tools) from the inline module to
tests/component/gemini_capabilities.rs since they exercise the public
Provider trait contract through the factory. Add a new
convert_tools_returns_prompt_guided test verifying the agent loop will
receive PromptGuided payload for Gemini.

Private internals tests (Part serialization, build_parts, role mapping)
remain inline since those types are not publicly exported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(gemini): fix cargo fmt formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(gemini): add prompt_caching field to capabilities declaration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: myclaw <myclaw@myclaws-MacBook-Air.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 18:22:41 -04:00
Anton Markelov
96f25ac701
fix(prompt): respect autonomy level in SafetySection (Agent/gateway WS path) (#3952) (#4037)
The `SafetySection` in `SystemPromptBuilder` always hardcoded
"Do not run destructive commands without asking" and "Do not bypass
oversight or approval mechanisms" regardless of the configured
autonomy level. This caused the gateway WebSocket path (web interface)
to instruct the LLM to simulate approval dialogs even when
`autonomy.level = "full"`.

PRs #3955/#3970/#3975 fixed the channel dispatch path
(`build_system_prompt_with_mode_and_autonomy`) but missed the
`Agent::from_config` → `SystemPromptBuilder` path used by
`gateway/ws.rs`.

Changes:
- Add `autonomy_level` field to `PromptContext`
- Rewrite `SafetySection::build()` to conditionally include/exclude
  approval instructions based on autonomy level, matching the logic
  already present in `build_system_prompt_with_mode_and_autonomy`
- Add `autonomy_level` field to `Agent` struct and `AgentBuilder`
- Pass `config.autonomy.level` through `Agent::from_config`
- Add tests for full/supervised autonomy safety section behavior

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 18:22:35 -04:00
Jacobinwwey
fb5c8cb620
feat(tools): route web_search providers with alias fallback (#4038) 2026-03-20 18:22:32 -04:00
ifengqi
9f5543e046
fix(qq): respond to WebSocket Ping frames to prevent connection timeout (#4041)
The QQ channel WebSocket loop did not handle incoming Ping frames,
causing the server to consider the connection dead and drop it. Add a
Ping handler that replies with Pong, keeping the connection alive.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 18:22:30 -04:00
luikore
05c9b8180b
Make lark / feishu render markdown (#3866)
Co-authored-by: Luikore <masked>
2026-03-20 18:22:29 -04:00
Thorbjørn Lindeijer
f96a0471b5
fix(providers): clamp unsupported temperatures in Claude Code provider (#3961)
The Claude Code CLI only supports temperatures 0.7 and 1.0, but
internal subsystems (memory consolidation, context summarizer) use
lower values like 0.1 and 0.2. Previously the provider rejected these
with a hard error, triggering retries and WARN-level log noise.

Clamp to the nearest supported value instead, since the CLI ignores
temperature anyway.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 18:22:26 -04:00
Artem Chernenko
072f5f1170
fix(transcription): honor configured default provider (#3883)
Co-authored-by: Artem Chernenko <12207348+turboazot@users.noreply.github.com>
2026-03-20 18:22:25 -04:00
Darren.Zeng
28f94ae48c
style: enhance .editorconfig with comprehensive file type settings (#3872)
Expand the minimal .editorconfig to include comprehensive settings for
all file types used in the project:

- Add root = true declaration
- Add standard settings: end_of_line, charset, trim_trailing_whitespace,
  insert_final_newline
- Add Rust-specific settings (indent_size = 4, max_line_length = 100)
  to match rustfmt.toml
- Add Markdown settings (preserve trailing whitespace for hard breaks)
- Add TOML, YAML, Python, Shell script, and JSON settings

This ensures consistent editor behavior across all contributors and
matches the project's formatting standards.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-20 18:22:22 -04:00
RoomWithOutRoof
8a217a77f9
fix(config): add challenge_max_attempts field to OtpConfig (#3921)
* fix(docker): default CMD to daemon instead of gateway

- Change default Docker CMD from gateway to daemon in both dev and release stages
- gateway only starts the HTTP/WebSocket server — channel listeners (Matrix, Telegram, Discord, etc.) are never spawned
- daemon starts the full runtime: gateway + channels + heartbeat + scheduler
- Users who configure channels in config.toml and run the default image get no response because the channel sync loops never start

* fix(config): add challenge_max_attempts field to OtpConfig

Add missing challenge_max_attempts field to support OTP challenge
attempt limiting. This field allows users to configure the maximum
number of OTP challenge attempts before lockout.

Fixes #3919

---------

Co-authored-by: Jah-yee <jah.yee@outlook.com>
Co-authored-by: Jah-yee <jahyee@sparklab.ai>
2026-03-20 18:22:19 -04:00
Chris Hengge
79a7f08b04
fix(web): anchor memory table to viewport with dual scrollbars (#4027)
The /memory data grid grew unboundedly with table rows, pushing the
horizontal scrollbar to the very bottom of a tall page and making it
inaccessible without scrolling all the way down first.

- Layout: change outer shell from min-h-screen to h-screen +
  overflow-hidden, and add min-h-0 to <main> so flex-1 overflow-y-auto
  actually clamps at the viewport boundary instead of growing infinitely.
- Memory page: switch root div to flex-col h-full so it fills the
  bounded main area; give the glass-card table wrapper flex-1 min-h-0
  overflow-auto so it consumes remaining space and exposes both
  scrollbars without any page-level scrolling required.
- index.css: pin .table-electric thead th with position:sticky / top:0
  and a matching opaque background so column headers stay visible
  during vertical scroll inside the bounded card.

The result behaves like a bounded iframe: the table fills the available
screen, rows scroll vertically, wide columns scroll horizontally, and
both scrollbars are always reachable.
2026-03-20 18:22:11 -04:00
Argenis
de12055364
feat(slack): implement reaction support for Slack channel (#4091)
* feat(slack): implement reaction support with sanitized error responses

Add add_reaction() and remove_reaction() for Slack channel, with
unicode-to-Slack emoji mapping, idempotent error handling, and
sanitized API error responses matching the pattern used by
chat.postMessage.

Based on #4089 by @joehoyle, with sanitize_api_error() applied to
reaction error paths for consistency with existing Slack methods.

Supersedes #4089

* chore(deps): bump rustls-webpki to 0.103.10 (RUSTSEC-2026-0049)
2026-03-20 18:18:45 -04:00
Argenis
65c34966bb
Merge pull request #4092 from zeroclaw-labs/docs/aur-ssh-key
docs: remove architecture diagram from all READMEs
2026-03-20 18:02:32 -04:00
Will Sarg
4a2be7c2e5
feat(verifiable_intent): add native verifiable intent lifecycle module (#2938)
* feat(verifiable_intent): add native verifiable intent lifecycle module

Implements a Rust-native Verifiable Intent (VI) subsystem for ZeroClaw,
providing full credential lifecycle support for commerce agent authorization
using SD-JWT layered credentials.

New module: src/verifiable_intent/
- error.rs: ViError/ViErrorKind (25+ variants), implements std::error::Error
- types.rs: JWK, Cnf, Entity, Constraint (8 variants), Immediate/Autonomous
  mandate structs, Fulfillment, Layer1/Layer2/CredentialChain
- crypto.rs: base64url helpers, SD hash, JWS sign/verify, EC P-256 key
  generation/loading, disclosure creation, SD-JWT serialize/parse
- verification.rs: StrictnessMode, ChainVerificationResult,
  ConstraintCheckResult, verify_timestamps, verify_sd_hash_binding,
  verify_l3_cross_reference, verify_checkout_hash_binding, check_constraints
- issuance.rs: create_layer2_immediate, create_layer2_autonomous,
  create_layer3_payment, create_layer3_checkout

New tool: src/tools/verifiable_intent.rs
- VerifiableIntentTool implementing Tool trait (name: vi_verify)
- Operations: verify_binding, evaluate_constraints, verify_timestamps
- Gated behind verifiable_intent.enabled config flag

Wiring:
- src/lib.rs: pub mod verifiable_intent
- src/main.rs: mod verifiable_intent (binary re-declaration)
- src/config/schema.rs: VerifiableIntentConfig struct, field on Config
- src/config/mod.rs: re-exports VerifiableIntentConfig
- src/onboard/wizard.rs: default field in Config literals
- src/tools/mod.rs: conditional tool registration

Uses only existing deps: ring (ECDSA P-256), sha2, base64, serde_json,
chrono, anyhow. No new dependencies added.

Validation: cargo fmt clean, cargo clippy -D warnings clean,
cargo test --lib -- verifiable_intent passes (44 tests)

* chore(verifiable_intent): add Apache 2.0 attribution for VI spec reference

The src/verifiable_intent/ module is a Rust-native reimplementation based
on the Verifiable Intent open specification and reference implementation by
genereda (https://github.com/genereda/verifiable-intent), Apache 2.0.

- Add attribution section to src/verifiable_intent/mod.rs doc comment
- Add third-party attribution entry to NOTICE per Apache 2.0 section 4(d)

* fix(verifiable_intent): correct VI attribution URL and author

Replace hallucinated github.com/genereda/verifiable-intent with the
actual remote: github.com/agent-intent/verifiable-intent

* fix(verifiable_intent): remove unused pub re-exports to fix clippy

Remove unused re-exports of ViError, ViErrorKind, types::*,
ChainVerificationResult, and ConstraintCheckResult from the module
root. Only StrictnessMode is used externally.

---------

Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
2026-03-20 17:52:55 -04:00
argenis de la rosa
2bd141aa07 docs: remove architecture diagram section from all READMEs
Remove the "How it works (short)" ASCII diagram section from
all 31 README files (English + 30 translations).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 17:45:35 -04:00
Argenis
cc470601de
docs(i18n): expand language hubs and add six new locales (#2934)
Expand README/docs navigation to include Korean, Tagalog, German, Arabic, Hindi, and Bengali locale entries. Add canonical locale hub and summary files for each new language under docs/i18n/.

Update i18n index/coverage metadata to reflect hub-level support and keep language discovery consistent across root docs entry points.
2026-03-20 17:40:09 -04:00
Argenis
707ee02d76
chore: bump version to 0.5.4 (#4090) 2026-03-20 16:06:52 -04:00
Argenis
a47a9ee269
fix(skills): improve ClawhHub skill installer with zip crate and URL parsing (#4088)
Replace the shell-based unzip extraction with the zip crate for
cross-platform support. Use reqwest::Url for proper URL parsing,
add www.clawhub.ai and clawhub: shorthand support, fix the download
API URL, add ZIP path traversal protection, size limits, rate-limit
handling, and SKILL.toml fallback generation.

Supersedes #4043
Closes #4022
2026-03-20 15:46:52 -04:00
Argenis
8bb61fe368
fix(cron): persist delivery for api-created cron jobs (#4087)
Resolves merge conflicts from PR #4064. Uses typed DeliveryConfig in
CronAddBody and passes delivery directly to add_shell_job_with_approval
and add_agent_job instead of post-creation patching. Preserves master's
richer API fields (session_target, model, allowed_tools, delete_after_run).
2026-03-20 15:42:00 -04:00
avianion
38a8e910d0
feat(providers): add Avian as OpenAI-compatible provider (#4076)
* feat(providers): add Avian as a named provider

Add Avian (https://avian.io) as a first-class OpenAI-compatible provider
with bearer auth via AVIAN_API_KEY. Registers the provider in the factory,
credential resolver, provider list, onboard wizard, and docs.

Models: deepseek/deepseek-v3.2, moonshotai/kimi-k2.5, z-ai/glm-5,
minimax/minimax-m2.5.

* style: fix rustfmt formatting in wizard.rs curated models

Collapse short GLM-5 tuple onto single line to satisfy cargo fmt.

* fix: add none-backend test and sync localized docs

- Add memory_backend parameter to scaffold_workspace so it can
  conditionally skip MEMORY.md creation and adjust AGENTS.md guidance
  when memory.backend = "none"
- Add test scaffold_none_backend_disables_memory_guidance_and_skips_memory_md
- Sync Avian provider entry to all 6 localized providers-reference.md
  files (el, fr, ja, ru, vi, zh-CN)
- Bump providers-reference.md date to March 9, 2026

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(onboard): add explicit Avian assertions in wizard helper tests

Add targeted assertions for the Avian provider branches to prevent
silent regressions, as requested in CodeRabbit review feedback:

- default_model_for_provider("avian") => "deepseek/deepseek-v3.2"
- curated_models_for_provider("avian") includes all 4 catalog entries
- supports_live_model_fetch("avian") => true
- models_endpoint_for_provider("avian") => Avian API URL
- provider_env_var("avian") => "AVIAN_API_KEY"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in wizard.rs scaffold_workspace

Break scaffold_workspace function signature and .await.unwrap() chains
across multiple lines to comply with rustfmt max line width.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
2026-03-20 15:31:59 -04:00
Eddie's AI Agent
916ad490bd
fix: remove BSD stat fallback from Dockerfiles (#3847) (#4077)
Containers always run Linux, so only GNU stat (-c%s) is needed.
The BSD fallback (stat -f%z) caused shell arithmetic errors under
podman when the fallback syntax was evaluated.

Co-authored-by: SpaceLobster <spacelobster@SpaceLobsters-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
2026-03-20 15:31:56 -04:00
Eddie's AI Agent
2dee42d6e4
fix(daemon): add 8MB stack size for ARM64 Linux targets (#4078)
ARM64 Linux (musl and Android) targets were using the default 2MB stack,
which is insufficient for the 126 JsonSchema derives and causes silent
daemon crashes due to stack overflow. x86_64 and Windows already had 8MB
overrides.

Closes #3537

Co-authored-by: SpaceLobster <spacelobster@SpaceLobsters-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Argenis <theonlyhennygod@gmail.com>
2026-03-20 15:28:40 -04:00
dependabot[bot]
6dbc1d7c9c
chore(deps): bump the rust-all group with 2 updates (#4047)
Bumps the rust-all group with 2 updates: [opentelemetry-otlp](https://github.com/open-telemetry/opentelemetry-rust) and [extism](https://github.com/extism/extism).


Updates `opentelemetry-otlp` from 0.31.0 to 0.31.1
- [Release notes](https://github.com/open-telemetry/opentelemetry-rust/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-rust/blob/main/docs/release_0.30.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.31.0...opentelemetry-otlp-0.31.1)

Updates `extism` from 1.13.0 to 1.20.0
- [Release notes](https://github.com/extism/extism/releases)
- [Commits](https://github.com/extism/extism/compare/v1.13.0...v1.20.0)

---
updated-dependencies:
- dependency-name: opentelemetry-otlp
  dependency-version: 0.31.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-all
- dependency-name: extism
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 15:16:08 -04:00
Anatolii Fesiuk
2bc2ddfbae
feat(tool): add myself and list_projects actions to jira tool (#4061)
* Sync jira tool description between .rs and en.toml

Replace multi-line operational guide in en.toml with the same one-liner
already in jira_tool.rs description(), matching the pattern used by all
other tools where both sources are in sync.

* Add myself action to jira tool for credential verification

* Add tests for myself action in jira tool

* Review and fix list_projects action added to jira tool

- Fix doc comment: update action count from four to five and add missing
  myself entry
- Remove redundant statuses_url variable (was identical to url)

The list_projects action fetches all projects with their issue types,
statuses, and assignable users by combining /rest/api/3/project,
per-project /statuses, and /user/assignable/multiProjectSearch endpoints.

* Remove hardcoded project-specific statuses from shape_projects

Replace fixed known_order list (which included project-specific statuses
like 'Collecting Intel', 'Design', 'Verification') with a simple
alphabetical sort. Any Jira project can use arbitrary status names so
hardcoding an order is not applicable universally.

* Fix list_projects: bounded concurrency, error surfacing, and output shape

- Use tokio::task::JoinSet with STATUS_CONCURRENCY=5 to fetch per-project
  statuses concurrently instead of sequentially, bounding API blast radius
- Surface user fetch errors: non-2xx and JSON parse failures now bail
  instead of silently falling back to empty vec
- Surface per-project status JSON parse errors instead of swallowing them
  with unwrap_or_else
- Move users to top-level output {projects, users} so they are not
  duplicated across every project entry

* fix(tool): apply rustfmt formatting to jira_tool.rs
2026-03-20 15:11:53 -04:00
Moksh Gupta
9fadf50375
Feat/add pinggy tunnel (#4060)
* feat(tunnel): add Pinggy tunnel support with configuration options

* feat(pinggy): update Pinggy tunnel configuration to remove domain field and improve SSH command handling

* feat(pinggy): add encryption and decryption for Pinggy tunnel token in config

* feat(pinggy): enhance region configuration for Pinggy tunnel with detailed options and validation

* feat(pinggy): enhance region validation and streamline output handling in Pinggy tunnel

* fix(pinggy): resolve clippy and fmt warnings

---------

Co-authored-by: moksh gupta <moksh.gupta@linktoany.com>
2026-03-20 15:11:50 -04:00
tf4fun
a047a0d9b8
feat(channel): enhance QQ channel with rich media and cron delivery (#4059)
Add full rich media send/receive support using unified [TYPE:target] markers
(aligned with Telegram). Register QQ as a cron announcement delivery channel.

- Media upload with SHA256-based caching and TTL
- Attachment download to workspace with all types supported
- Voice: prefer voice_wav_url (WAV), inject QQ ASR transcription
- File uploads include file_name for proper display in QQ client
- msg_seq generation and reply rate-limit tracking
- QQ delivery instructions in system prompt
- Register QQ in cron scheduler and tool description

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:11:47 -04:00
Argenis
43be8e5075
fix: preserve Slack session context when thread_replies=false (#4084)
* fix: preserve session context across Slack messages when thread_replies=false

When thread_replies=false, inbound_thread_ts() was falling back to each
message's own ts, giving every message a unique conversation key and
breaking multi-turn context. Now top-level messages get thread_ts=None
when threading is disabled, so all messages from the same user in the
same channel share one session.

Closes #4052

* chore: ignore RUSTSEC-2024-0384 (unmaintained instant crate via nostr)
2026-03-20 15:00:31 -04:00
Argenis
ea8fe95b19
fix: normalize 5-field cron weekday numbers to standard crontab semantics (#4082)
* fix: normalize 5-field cron weekday numbers to match standard crontab

The cron crate uses 1=Sun,2=Mon,...,7=Sat while standard crontab uses
0/7=Sun,1=Mon,...,6=Sat. This caused '1-5' to mean Sun-Thu instead of
Mon-Fri. Add a normalization step when converting 5-field expressions
to 6-field so user-facing semantics match standard crontab behavior.

Closes #4049

* chore: ignore RUSTSEC-2024-0384 (unmaintained instant crate via nostr)
2026-03-20 15:00:28 -04:00
Argenis
ce8f2133fb
fix(provider): replace overall timeout with per-read timeout for Codex streams (#4081)
* fix(provider): replace overall timeout with per-read timeout for Codex streams

The 120-second overall HTTP timeout was killing SSE streams mid-flight
when GPT-5.4 reasoning responses exceeded that duration. Replace with
a 300-second per-read timeout that only fires when no data arrives,
allowing long-running streams to complete while still detecting stalled
connections.

Closes #3786

* chore(deps): bump aws-lc-rs to fix RUSTSEC-2026-0044/0048

Update aws-lc-rs 1.16.1 → 1.16.2 (aws-lc-sys 0.38.0 → 0.39.0) to
resolve security advisory for X.509 Name Constraints Bypass.
2026-03-20 14:40:27 -04:00
Argenis
19d9c6f32c
Merge pull request #4075 from zeroclaw-labs/feat/testing-agent-loop
chore: bump version to 0.5.3
2026-03-20 13:34:04 -04:00
Argenis
6f5b029033
fix(skills): support ClawHub registry URLs in skills install (#4069)
`zeroclaw skills install https://clawhub.ai/owner/skill` previously
failed because is_git_source() treated all https:// URLs as git repos.
ClawHub is a web registry, not a git host.

- Add is_clawhub_source() to detect clawhub.ai URLs
- Add clawhub_slug() to extract the skill name from the URL path
- Add install_clawhub_skill_source() to download via the ClawHub
  download API and extract the ZIP into the skills directory
- Exclude clawhub.ai URLs from git source detection
- Security audit runs on downloaded skills as with git installs

Closes #4022
2026-03-20 12:19:14 -04:00
Argenis
83ee103abb
fix: add interrupt_on_new_message support for Matrix channel (#4070)
Add the missing interrupt_on_new_message field to MatrixConfig and wire
it through InterruptOnNewMessageConfig so Matrix behaves consistently
with Telegram, Slack, Discord, and Mattermost.

Closes #4058
2026-03-20 12:17:16 -04:00
Argenis
d39ba69156
fix(providers): bail immediately on unrecoverable context window overflow (#4068)
When a request exceeds a model's context window and there is no
conversation history to truncate (e.g. system prompt alone is too
large), bail immediately with an actionable error message instead of
wasting all retry attempts on the same unrecoverable error.

Previously, the retry loop would attempt truncation, find nothing to
drop (only system + one user message), then fall through to the normal
retry logic which classified context window errors as retryable. This
caused 3 identical failing API calls for a single "hello" message.

The fix adds an early exit in all three chat methods (chat_with_history,
chat_with_tools, chat) when truncate_for_context returns 0 dropped
messages, matching the existing behavior in chat_with_system.

Fixes #4044
2026-03-20 12:10:24 -04:00
Argenis
22cdb5e2e2
fix(whatsapp): remove duplicate variable declaration causing unused warning (#4066)
* feat(config): add google workspace operation allowlists

* docs(superpowers): link google workspace operation inventory sources

* docs(superpowers): verify starter operation examples

* fix(google_workspace): remove duplicate credential/audit blocks, fix trim in allowlist check, add duplicate-methods test

- Remove the duplicated credentials_path, default_account, and audit_log
  blocks that were copy-pasted into execute() — they were idempotent but
  misleading and would double-append --account args on every call.
- Trim stored service/resource/method values in is_operation_allowed() to
  match the trim applied during Config::validate(), preventing a mismatch
  where a config entry with surrounding whitespace would pass validation but
  never match at runtime.
- Add google_workspace_allowed_operations_reject_duplicate_methods_within_entry
  test to cover the duplicate-method validation path that was implemented but
  untested.

* fix(google_workspace): close sub_resource bypass, trim allowed_services at runtime, mark spec implemented

- HIGH: extract and validate sub_resource before the allowlist check;
  is_operation_allowed() now accepts Option<&str> for sub_resource and
  returns false (fail-closed) when allowed_operations is non-empty and
  a sub_resource is present — prevents nested gws calls such as
  `drive/files/permissions/list` from slipping past a 3-segment policy
- MEDIUM: runtime allowed_services check now uses s.trim() == service,
  matching the trim() applied during config validation
- LOW: spec status updated to Implemented; stale "does not currently
  support method-level allowlists" line removed
- Added test: operation_allowlist_rejects_sub_resource_when_operations_configured

* docs(google_workspace): document sub_resource limitation and add config-reference entries

Spec updates (superpowers/specs):
- Semantics section: note that sub_resource calls are denied fail-closed when
  allowed_operations is configured
- Mental model: show both 3-segment and 4-segment gws command shapes; explain
  that 4-segment commands are unsupported with allowed_operations in this version
- Runtime enforcement: correct the validation order to match the implementation
  (sub_resource extracted before allowlist check, budget charged last)
- New section: Sub-Resource Limitation — documents impact, operator workaround,
  and confirms the deny is intentional for this slice
- Follow-On Work: add sub_resource config model extension as item 1

Config reference updates (all three locales):
- Add [google_workspace] section with top-level keys, [[allowed_operations]]
  sub-table, sub-resource limitation note, and TOML example

* fix(docs): add classroom and events to allowed_services list in all config-reference locales

* feat(google_workspace): extend allowed_operations to support sub_resource for 4-segment gws commands

All Gmail operations use gws gmail users <sub_resource> <method>, not the flat
3-segment shape. Without sub_resource support in allowed_operations, Gmail could
not be scoped at all, making the email-assistant use case impossible.

Config model:
- Add optional sub_resource field to GoogleWorkspaceAllowedOperation
- An entry without sub_resource matches 3-segment calls (Drive, Calendar, etc.)
- An entry with sub_resource matches only calls with that exact sub_resource value
- Duplicate detection updated to (service, resource, sub_resource) key

Runtime:
- Remove blanket sub_resource deny; is_operation_allowed now matches on all four
  dimensions including the optional sub_resource

Tests:
- Add operation_allowlist_matches_gmail_sub_resource_shape
- Add operation_allowlist_matches_drive_3_segment_shape
- Add rejects_operation_with_unlisted_sub_resource
- Add google_workspace_allowed_operations_allow_same_resource_different_sub_resource
- Add google_workspace_allowed_operations_reject_invalid_sub_resource_characters
- Add google_workspace_allowed_operations_deserialize_without_sub_resource
- Update all existing tests to use correct gws command shapes

Docs:
- Spec: correct Gmail examples throughout; remove Sub-Resource Limitation section;
  update data model, validation rules, example use case, and follow-on work
- Config-reference (en, vi, zh-CN): add sub_resource field to allowed_operations
  table; update Gmail examples to correct 4-segment shapes

Platform:
- email-assistant SKILL.md: update allowed_operations paths to gmail/users/* shape

* fix(google_workspace): add classroom and events to service parameter schema description

* fix(google_workspace): cross-validate allowed_operations service against allowed_services

When allowed_services is explicitly configured, each allowed_operations entry's
service must appear in that list. An entry that can never match at runtime is a
misconfigured policy: it looks valid but silently produces a narrower scope than
the operator intended. Validation now rejects it with a clear error message.

Scope: only applies when allowed_services is non-empty. When it is empty, the tool
uses a built-in default list defined in the tool layer; the validator cannot
enumerate that list without duplicating the constant, so the cross-check is skipped.

Also:
- Update allowed_operations field doc-comment from 3-part (service, resource, method)
  to 4-part (service, resource, sub_resource, method) model
- Soften Gmail sub_resource "required" language in config-reference (en, vi, zh-CN)
  from a validation requirement to a runtime matching requirement — the validator
  does not and should not hardcode API shape knowledge for individual services
- Add tests: rejects operation service not in allowed_services; skips cross-check
  when allowed_services is empty

* fix(google_workspace): cross-validate allowed_operations.service against effective service set

When allowed_services is empty the validator was silently skipping the
service cross-check, allowing impossible configs like an unlisted service
in allowed_operations to pass validation and only fail at runtime.

Move DEFAULT_GWS_SERVICES from the tool layer (google_workspace.rs) into
schema.rs so the validator can use it unconditionally. When allowed_services
is explicitly set, validate against that set; when empty, fall back to
DEFAULT_GWS_SERVICES. Remove the now-incorrect "skips cross-check when empty"
test and add two replacement tests: one confirming a valid default service
passes, one confirming an unknown service is rejected even with empty
allowed_services.

* fix(google_workspace): update test assertion for new error message wording

* docs(google_workspace): fix stale 3-segment gmail example in TDD plan

* fix(google_workspace): address adversarial review round 4 findings

- Error message for denied operations now includes sub_resource when
  present, so gmail/users/messages/send and gmail/users/drafts/send
  produce distinct, debuggable errors.
- Audit log now records sub_resource, completing the trail for 4-segment
  Gmail operations.
- Normalize (trim) allowed_services and allowed_operations fields at
  construction time in new(). Runtime comparisons now use plain equality
  instead of .trim() on every call, removing the latent defect where a
  future code path could forget to trim and silently fail to match.
- Unify runtime character validation with schema validation: sub_resource
  and service/resource/method checks now both require lowercase alphanumeric
  plus underscore and hyphen, matching the validator's character set.
- Add positional_cmd_args() test helper and tests verifying 3-segment
  (Drive) and 4-segment (Gmail) argument ordering.
- Add test confirming page_limit without page_all passes validation.
- Add test confirming whitespace in config values is normalized at
  construction, not deferred to comparison time.
- Fix spec Runtime Enforcement section to reflect actual code order.

* fix(google_workspace): wire production helpers to close test coverage gaps

- Remove #[cfg(test)] from positional_cmd_args; execute() now calls the
  same function the arg-ordering tests exercise, so a drift in the real
  command-building path is caught by the existing tests.
- Extract build_pagination_args(page_all, page_limit) as a production
  method used by execute(). Replace the brittle page_limit_without_page_all
  test (which relied on environment-specific execution failure wording)
  with four direct assertions on build_pagination_args covering all
  page_all/page_limit combinations.

* fix(whatsapp): remove duplicate variable declaration causing unused warning

Remove duplicate `let transcription_config = self.transcription.clone()`
(line 626 shadowed by identical line 628). The duplicate caused a
compiler warning during --features whatsapp-web builds.

Note: the reported "hang" at 526/528 crates is expected behavior for
release builds with lto="fat" + codegen-units=1 — the final link step
is slow but does complete.

Closes #4034

---------

Co-authored-by: Nim G <theredspoon@users.noreply.github.com>
2026-03-20 12:07:55 -04:00
Argenis
206d19af11
fix(api): respect job_type and delivery in POST /api/cron (#4063) (#4065) 2026-03-20 12:03:46 -04:00
Nim G
bbd2556861
feat(tool): google_workspace operation-level allowlist (#4010)
* feat(config): add google workspace operation allowlists

* docs(superpowers): link google workspace operation inventory sources

* docs(superpowers): verify starter operation examples

* fix(google_workspace): remove duplicate credential/audit blocks, fix trim in allowlist check, add duplicate-methods test

- Remove the duplicated credentials_path, default_account, and audit_log
  blocks that were copy-pasted into execute() — they were idempotent but
  misleading and would double-append --account args on every call.
- Trim stored service/resource/method values in is_operation_allowed() to
  match the trim applied during Config::validate(), preventing a mismatch
  where a config entry with surrounding whitespace would pass validation but
  never match at runtime.
- Add google_workspace_allowed_operations_reject_duplicate_methods_within_entry
  test to cover the duplicate-method validation path that was implemented but
  untested.

* fix(google_workspace): close sub_resource bypass, trim allowed_services at runtime, mark spec implemented

- HIGH: extract and validate sub_resource before the allowlist check;
  is_operation_allowed() now accepts Option<&str> for sub_resource and
  returns false (fail-closed) when allowed_operations is non-empty and
  a sub_resource is present — prevents nested gws calls such as
  `drive/files/permissions/list` from slipping past a 3-segment policy
- MEDIUM: runtime allowed_services check now uses s.trim() == service,
  matching the trim() applied during config validation
- LOW: spec status updated to Implemented; stale "does not currently
  support method-level allowlists" line removed
- Added test: operation_allowlist_rejects_sub_resource_when_operations_configured

* docs(google_workspace): document sub_resource limitation and add config-reference entries

Spec updates (superpowers/specs):
- Semantics section: note that sub_resource calls are denied fail-closed when
  allowed_operations is configured
- Mental model: show both 3-segment and 4-segment gws command shapes; explain
  that 4-segment commands are unsupported with allowed_operations in this version
- Runtime enforcement: correct the validation order to match the implementation
  (sub_resource extracted before allowlist check, budget charged last)
- New section: Sub-Resource Limitation — documents impact, operator workaround,
  and confirms the deny is intentional for this slice
- Follow-On Work: add sub_resource config model extension as item 1

Config reference updates (all three locales):
- Add [google_workspace] section with top-level keys, [[allowed_operations]]
  sub-table, sub-resource limitation note, and TOML example

* fix(docs): add classroom and events to allowed_services list in all config-reference locales

* feat(google_workspace): extend allowed_operations to support sub_resource for 4-segment gws commands

All Gmail operations use gws gmail users <sub_resource> <method>, not the flat
3-segment shape. Without sub_resource support in allowed_operations, Gmail could
not be scoped at all, making the email-assistant use case impossible.

Config model:
- Add optional sub_resource field to GoogleWorkspaceAllowedOperation
- An entry without sub_resource matches 3-segment calls (Drive, Calendar, etc.)
- An entry with sub_resource matches only calls with that exact sub_resource value
- Duplicate detection updated to (service, resource, sub_resource) key

Runtime:
- Remove blanket sub_resource deny; is_operation_allowed now matches on all four
  dimensions including the optional sub_resource

Tests:
- Add operation_allowlist_matches_gmail_sub_resource_shape
- Add operation_allowlist_matches_drive_3_segment_shape
- Add rejects_operation_with_unlisted_sub_resource
- Add google_workspace_allowed_operations_allow_same_resource_different_sub_resource
- Add google_workspace_allowed_operations_reject_invalid_sub_resource_characters
- Add google_workspace_allowed_operations_deserialize_without_sub_resource
- Update all existing tests to use correct gws command shapes

Docs:
- Spec: correct Gmail examples throughout; remove Sub-Resource Limitation section;
  update data model, validation rules, example use case, and follow-on work
- Config-reference (en, vi, zh-CN): add sub_resource field to allowed_operations
  table; update Gmail examples to correct 4-segment shapes

Platform:
- email-assistant SKILL.md: update allowed_operations paths to gmail/users/* shape

* fix(google_workspace): add classroom and events to service parameter schema description

* fix(google_workspace): cross-validate allowed_operations service against allowed_services

When allowed_services is explicitly configured, each allowed_operations entry's
service must appear in that list. An entry that can never match at runtime is a
misconfigured policy: it looks valid but silently produces a narrower scope than
the operator intended. Validation now rejects it with a clear error message.

Scope: only applies when allowed_services is non-empty. When it is empty, the tool
uses a built-in default list defined in the tool layer; the validator cannot
enumerate that list without duplicating the constant, so the cross-check is skipped.

Also:
- Update allowed_operations field doc-comment from 3-part (service, resource, method)
  to 4-part (service, resource, sub_resource, method) model
- Soften Gmail sub_resource "required" language in config-reference (en, vi, zh-CN)
  from a validation requirement to a runtime matching requirement — the validator
  does not and should not hardcode API shape knowledge for individual services
- Add tests: rejects operation service not in allowed_services; skips cross-check
  when allowed_services is empty

* fix(google_workspace): cross-validate allowed_operations.service against effective service set

When allowed_services is empty the validator was silently skipping the
service cross-check, allowing impossible configs like an unlisted service
in allowed_operations to pass validation and only fail at runtime.

Move DEFAULT_GWS_SERVICES from the tool layer (google_workspace.rs) into
schema.rs so the validator can use it unconditionally. When allowed_services
is explicitly set, validate against that set; when empty, fall back to
DEFAULT_GWS_SERVICES. Remove the now-incorrect "skips cross-check when empty"
test and add two replacement tests: one confirming a valid default service
passes, one confirming an unknown service is rejected even with empty
allowed_services.

* fix(google_workspace): update test assertion for new error message wording

* docs(google_workspace): fix stale 3-segment gmail example in TDD plan

* fix(google_workspace): address adversarial review round 4 findings

- Error message for denied operations now includes sub_resource when
  present, so gmail/users/messages/send and gmail/users/drafts/send
  produce distinct, debuggable errors.
- Audit log now records sub_resource, completing the trail for 4-segment
  Gmail operations.
- Normalize (trim) allowed_services and allowed_operations fields at
  construction time in new(). Runtime comparisons now use plain equality
  instead of .trim() on every call, removing the latent defect where a
  future code path could forget to trim and silently fail to match.
- Unify runtime character validation with schema validation: sub_resource
  and service/resource/method checks now both require lowercase alphanumeric
  plus underscore and hyphen, matching the validator's character set.
- Add positional_cmd_args() test helper and tests verifying 3-segment
  (Drive) and 4-segment (Gmail) argument ordering.
- Add test confirming page_limit without page_all passes validation.
- Add test confirming whitespace in config values is normalized at
  construction, not deferred to comparison time.
- Fix spec Runtime Enforcement section to reflect actual code order.

* fix(google_workspace): wire production helpers to close test coverage gaps

- Remove #[cfg(test)] from positional_cmd_args; execute() now calls the
  same function the arg-ordering tests exercise, so a drift in the real
  command-building path is caught by the existing tests.
- Extract build_pagination_args(page_all, page_limit) as a production
  method used by execute(). Replace the brittle page_limit_without_page_all
  test (which relied on environment-specific execution failure wording)
  with four direct assertions on build_pagination_args covering all
  page_all/page_limit combinations.

---------

Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
2026-03-20 11:46:22 -04:00
368 changed files with 61495 additions and 5426 deletions

View File

@ -7,4 +7,6 @@ ignore = [
"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
# instant crate unmaintained — transitive dep via nostr; no upstream fix
"RUSTSEC-2024-0384",
]

View File

@ -2,7 +2,7 @@
rustflags = ["-C", "link-arg=-static"]
[target.aarch64-unknown-linux-musl]
rustflags = ["-C", "link-arg=-static"]
rustflags = ["-C", "link-arg=-static", "-C", "link-arg=-Wl,-z,stack-size=8388608"]
# Android targets (NDK toolchain)
[target.armv7-linux-androideabi]
@ -10,3 +10,4 @@ linker = "armv7a-linux-androideabi21-clang"
[target.aarch64-linux-android]
linker = "aarch64-linux-android21-clang"
rustflags = ["-C", "link-arg=-Wl,-z,stack-size=8388608"]

View File

@ -1,3 +1,44 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# All files
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# Rust files - match rustfmt.toml
[*.rs]
indent_size = 4
max_line_length = 100
# Markdown files
[*.md]
trim_trailing_whitespace = false
max_line_length = 80
# TOML files
[*.toml]
indent_size = 2
# YAML files
[*.{yml,yaml}]
indent_size = 2
# Python files
[*.py]
indent_size = 4
max_line_length = 100
# Shell scripts
[*.{sh,bash}]
indent_size = 2
# JSON files
[*.json]
indent_size = 2

View File

@ -118,3 +118,7 @@ PROVIDER=openrouter
# Optional: Brave Search (requires API key from https://brave.com/search/api)
# WEB_SEARCH_PROVIDER=brave
# BRAVE_API_KEY=your-brave-search-api-key
#
# Optional: SearXNG (self-hosted, requires instance URL)
# WEB_SEARCH_PROVIDER=searxng
# SEARXNG_INSTANCE_URL=https://searx.example.com

301
.github/labeler.yml vendored
View File

@ -36,6 +36,145 @@
- any-glob-to-any-file:
- "src/channels/**"
"channel:bluesky":
- changed-files:
- any-glob-to-any-file:
- "src/channels/bluesky.rs"
"channel:clawdtalk":
- changed-files:
- any-glob-to-any-file:
- "src/channels/clawdtalk.rs"
"channel:cli":
- changed-files:
- any-glob-to-any-file:
- "src/channels/cli.rs"
"channel:dingtalk":
- changed-files:
- any-glob-to-any-file:
- "src/channels/dingtalk.rs"
"channel:discord":
- changed-files:
- any-glob-to-any-file:
- "src/channels/discord.rs"
- "src/channels/discord_history.rs"
"channel:email":
- changed-files:
- any-glob-to-any-file:
- "src/channels/email_channel.rs"
- "src/channels/gmail_push.rs"
"channel:imessage":
- changed-files:
- any-glob-to-any-file:
- "src/channels/imessage.rs"
"channel:irc":
- changed-files:
- any-glob-to-any-file:
- "src/channels/irc.rs"
"channel:lark":
- changed-files:
- any-glob-to-any-file:
- "src/channels/lark.rs"
"channel:linq":
- changed-files:
- any-glob-to-any-file:
- "src/channels/linq.rs"
"channel:matrix":
- changed-files:
- any-glob-to-any-file:
- "src/channels/matrix.rs"
"channel:mattermost":
- changed-files:
- any-glob-to-any-file:
- "src/channels/mattermost.rs"
"channel:mochat":
- changed-files:
- any-glob-to-any-file:
- "src/channels/mochat.rs"
"channel:mqtt":
- changed-files:
- any-glob-to-any-file:
- "src/channels/mqtt.rs"
"channel:nextcloud-talk":
- changed-files:
- any-glob-to-any-file:
- "src/channels/nextcloud_talk.rs"
"channel:nostr":
- changed-files:
- any-glob-to-any-file:
- "src/channels/nostr.rs"
"channel:notion":
- changed-files:
- any-glob-to-any-file:
- "src/channels/notion.rs"
"channel:qq":
- changed-files:
- any-glob-to-any-file:
- "src/channels/qq.rs"
"channel:reddit":
- changed-files:
- any-glob-to-any-file:
- "src/channels/reddit.rs"
"channel:signal":
- changed-files:
- any-glob-to-any-file:
- "src/channels/signal.rs"
"channel:slack":
- changed-files:
- any-glob-to-any-file:
- "src/channels/slack.rs"
"channel:telegram":
- changed-files:
- any-glob-to-any-file:
- "src/channels/telegram.rs"
"channel:twitter":
- changed-files:
- any-glob-to-any-file:
- "src/channels/twitter.rs"
"channel:wati":
- changed-files:
- any-glob-to-any-file:
- "src/channels/wati.rs"
"channel:webhook":
- changed-files:
- any-glob-to-any-file:
- "src/channels/webhook.rs"
"channel:wecom":
- changed-files:
- any-glob-to-any-file:
- "src/channels/wecom.rs"
"channel:whatsapp":
- changed-files:
- any-glob-to-any-file:
- "src/channels/whatsapp.rs"
- "src/channels/whatsapp_storage.rs"
- "src/channels/whatsapp_web.rs"
"gateway":
- changed-files:
- any-glob-to-any-file:
@ -101,6 +240,73 @@
- any-glob-to-any-file:
- "src/providers/**"
"provider:anthropic":
- changed-files:
- any-glob-to-any-file:
- "src/providers/anthropic.rs"
"provider:azure-openai":
- changed-files:
- any-glob-to-any-file:
- "src/providers/azure_openai.rs"
"provider:bedrock":
- changed-files:
- any-glob-to-any-file:
- "src/providers/bedrock.rs"
"provider:claude-code":
- changed-files:
- any-glob-to-any-file:
- "src/providers/claude_code.rs"
"provider:compatible":
- changed-files:
- any-glob-to-any-file:
- "src/providers/compatible.rs"
"provider:copilot":
- changed-files:
- any-glob-to-any-file:
- "src/providers/copilot.rs"
"provider:gemini":
- changed-files:
- any-glob-to-any-file:
- "src/providers/gemini.rs"
- "src/providers/gemini_cli.rs"
"provider:glm":
- changed-files:
- any-glob-to-any-file:
- "src/providers/glm.rs"
"provider:kilocli":
- changed-files:
- any-glob-to-any-file:
- "src/providers/kilocli.rs"
"provider:ollama":
- changed-files:
- any-glob-to-any-file:
- "src/providers/ollama.rs"
"provider:openai":
- changed-files:
- any-glob-to-any-file:
- "src/providers/openai.rs"
- "src/providers/openai_codex.rs"
"provider:openrouter":
- changed-files:
- any-glob-to-any-file:
- "src/providers/openrouter.rs"
"provider:telnyx":
- changed-files:
- any-glob-to-any-file:
- "src/providers/telnyx.rs"
"service":
- changed-files:
- any-glob-to-any-file:
@ -121,6 +327,101 @@
- any-glob-to-any-file:
- "src/tools/**"
"tool:browser":
- changed-files:
- any-glob-to-any-file:
- "src/tools/browser.rs"
- "src/tools/browser_delegate.rs"
- "src/tools/browser_open.rs"
- "src/tools/text_browser.rs"
- "src/tools/screenshot.rs"
"tool:composio":
- changed-files:
- any-glob-to-any-file:
- "src/tools/composio.rs"
"tool:cron":
- changed-files:
- any-glob-to-any-file:
- "src/tools/cron_add.rs"
- "src/tools/cron_list.rs"
- "src/tools/cron_remove.rs"
- "src/tools/cron_run.rs"
- "src/tools/cron_runs.rs"
- "src/tools/cron_update.rs"
"tool:file":
- changed-files:
- any-glob-to-any-file:
- "src/tools/file_edit.rs"
- "src/tools/file_read.rs"
- "src/tools/file_write.rs"
- "src/tools/glob_search.rs"
- "src/tools/content_search.rs"
"tool:google-workspace":
- changed-files:
- any-glob-to-any-file:
- "src/tools/google_workspace.rs"
"tool:mcp":
- changed-files:
- any-glob-to-any-file:
- "src/tools/mcp_client.rs"
- "src/tools/mcp_deferred.rs"
- "src/tools/mcp_protocol.rs"
- "src/tools/mcp_tool.rs"
- "src/tools/mcp_transport.rs"
"tool:memory":
- changed-files:
- any-glob-to-any-file:
- "src/tools/memory_forget.rs"
- "src/tools/memory_recall.rs"
- "src/tools/memory_store.rs"
"tool:microsoft365":
- changed-files:
- any-glob-to-any-file:
- "src/tools/microsoft365/**"
"tool:shell":
- changed-files:
- any-glob-to-any-file:
- "src/tools/shell.rs"
- "src/tools/node_tool.rs"
- "src/tools/cli_discovery.rs"
"tool:sop":
- changed-files:
- any-glob-to-any-file:
- "src/tools/sop_advance.rs"
- "src/tools/sop_approve.rs"
- "src/tools/sop_execute.rs"
- "src/tools/sop_list.rs"
- "src/tools/sop_status.rs"
"tool:web":
- changed-files:
- any-glob-to-any-file:
- "src/tools/web_fetch.rs"
- "src/tools/web_search_tool.rs"
- "src/tools/web_search_provider_routing.rs"
- "src/tools/http_request.rs"
"tool:security":
- changed-files:
- any-glob-to-any-file:
- "src/tools/security_ops.rs"
- "src/tools/verifiable_intent.rs"
"tool:cloud":
- changed-files:
- any-glob-to-any-file:
- "src/tools/cloud_ops.rs"
- "src/tools/cloud_patterns.rs"
"tunnel":
- changed-files:
- any-glob-to-any-file:

View File

@ -7,7 +7,7 @@ on:
branches: [master]
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
group: ci-${{ github.event.pull_request.number || 'push-master' }}
cancel-in-progress: true
permissions:
@ -154,7 +154,7 @@ jobs:
run: mkdir -p web/dist && touch web/dist/.gitkeep
- name: Check all features
run: cargo check --all-features --locked
run: cargo check --features ci-all --locked
docs-quality:
name: Docs Quality

19
.github/workflows/pr-path-labeler.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: PR Path Labeler
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
label:
name: Apply path labels
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
with:
sync-labels: true

View File

@ -1,6 +1,22 @@
name: Pub Homebrew Core
on:
workflow_call:
inputs:
release_tag:
description: "Existing release tag to publish (vX.Y.Z)"
required: true
type: string
dry_run:
description: "Patch formula only (no push/PR)"
required: false
default: false
type: boolean
secrets:
HOMEBREW_UPSTREAM_PR_TOKEN:
required: false
HOMEBREW_CORE_BOT_TOKEN:
required: false
workflow_dispatch:
inputs:
release_tag:

View File

@ -19,6 +19,7 @@ env:
jobs:
detect-version-change:
name: Detect Version Bump
if: github.repository == 'zeroclaw-labs/zeroclaw'
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.check.outputs.changed }}
@ -40,6 +41,14 @@ jobs:
echo "Current version: ${current}"
echo "Previous version: ${previous}"
# Skip if stable release workflow will handle this version
# (indicated by an existing or imminent stable tag)
if git ls-remote --exit-code --tags origin "refs/tags/v${current}" >/dev/null 2>&1; then
echo "Stable tag v${current} exists — stable release workflow handles crates.io"
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$current" != "$previous" && -n "$current" ]]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "version=${current}" >> "$GITHUB_OUTPUT"
@ -102,6 +111,22 @@ jobs:
- name: Clean web build artifacts
run: rm -rf web/node_modules web/src web/package.json web/package-lock.json web/tsconfig*.json web/vite.config.ts web/index.html
- name: Publish aardvark-sys to crates.io
shell: bash
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
OUTPUT=$(cargo publish --locked --allow-dirty --no-verify -p aardvark-sys 2>&1) && exit 0
echo "$OUTPUT"
if echo "$OUTPUT" | grep -q 'already exists'; then
echo "::notice::aardvark-sys already on crates.io — skipping"
exit 0
fi
exit 1
- name: Wait for aardvark-sys to index
run: sleep 15
- name: Publish to crates.io
shell: bash
env:

View File

@ -67,6 +67,24 @@ jobs:
- name: Clean web build artifacts
run: rm -rf web/node_modules web/src web/package.json web/package-lock.json web/tsconfig*.json web/vite.config.ts web/index.html
- name: Publish aardvark-sys to crates.io
if: "!inputs.dry_run"
shell: bash
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
OUTPUT=$(cargo publish --locked --allow-dirty --no-verify -p aardvark-sys 2>&1) && exit 0
echo "$OUTPUT"
if echo "$OUTPUT" | grep -q 'already exists'; then
echo "::notice::aardvark-sys already on crates.io — skipping"
exit 0
fi
exit 1
- name: Wait for aardvark-sys to index
if: "!inputs.dry_run"
run: sleep 15
- name: Publish (dry run)
if: inputs.dry_run
run: cargo publish --dry-run --locked --allow-dirty --no-verify

View File

@ -21,25 +21,48 @@ env:
jobs:
version:
name: Resolve Version
if: github.repository == 'zeroclaw-labs/zeroclaw'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
tag: ${{ steps.ver.outputs.tag }}
skip: ${{ steps.ver.outputs.skip }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 2
- name: Compute beta version
id: ver
shell: bash
run: |
set -euo pipefail
base_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
# Skip beta if this is a version bump commit (stable release handles it)
commit_msg=$(git log -1 --pretty=format:"%s")
if [[ "$commit_msg" =~ ^chore:\ bump\ version ]]; then
echo "Version bump commit detected — skipping beta release"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Skip beta if a stable tag already exists for this version
if git ls-remote --exit-code --tags origin "refs/tags/v${base_version}" >/dev/null 2>&1; then
echo "Stable tag v${base_version} exists — skipping beta release"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
beta_tag="v${base_version}-beta.${GITHUB_RUN_NUMBER}"
echo "version=${base_version}" >> "$GITHUB_OUTPUT"
echo "tag=${beta_tag}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "Beta release: ${beta_tag}"
release-notes:
name: Generate Release Notes
needs: [version]
if: github.repository == 'zeroclaw-labs/zeroclaw' && needs.version.outputs.skip != 'true'
runs-on: ubuntu-latest
outputs:
notes: ${{ steps.notes.outputs.body }}
@ -130,6 +153,8 @@ jobs:
web:
name: Build Web Dashboard
needs: [version]
if: github.repository == 'zeroclaw-labs/zeroclaw' && needs.version.outputs.skip != 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
@ -241,9 +266,65 @@ jobs:
path: zeroclaw-${{ matrix.target }}.${{ matrix.ext }}
retention-days: 7
build-desktop:
name: Build Desktop App (macOS Universal)
needs: [version]
if: needs.version.outputs.skip != 'true'
runs-on: macos-14
timeout-minutes: 40
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
prefix-key: macos-tauri
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Tauri CLI
run: cargo install tauri-cli --locked
- name: Sync Tauri version with Cargo.toml
shell: bash
run: |
VERSION=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
cd apps/tauri
if command -v jq >/dev/null 2>&1; then
jq --arg v "$VERSION" '.version = $v' tauri.conf.json > tmp.json && mv tmp.json tauri.conf.json
else
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" tauri.conf.json
fi
echo "Tauri version set to: $VERSION"
- name: Build Tauri app (universal binary)
working-directory: apps/tauri
run: cargo tauri build --target universal-apple-darwin
- name: Prepare desktop release assets
run: |
mkdir -p desktop-assets
find target -name '*.dmg' -exec cp {} desktop-assets/ZeroClaw.dmg \; 2>/dev/null || true
find target -name '*.app.tar.gz' -exec cp {} desktop-assets/ZeroClaw-macos.app.tar.gz \; 2>/dev/null || true
find target -name '*.app.tar.gz.sig' -exec cp {} desktop-assets/ZeroClaw-macos.app.tar.gz.sig \; 2>/dev/null || true
echo "--- Desktop assets ---"
ls -lh desktop-assets/
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: desktop-macos
path: desktop-assets/*
retention-days: 7
publish:
name: Publish Beta Release
needs: [version, release-notes, build]
needs: [version, release-notes, build, build-desktop]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -253,16 +334,21 @@ jobs:
pattern: zeroclaw-*
path: artifacts
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-macos
path: artifacts/desktop-macos
- name: Generate checksums
run: |
cd artifacts
find . -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
find . -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.dmg' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
cat SHA256SUMS
- name: Collect release assets
run: |
mkdir -p release-assets
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name 'SHA256SUMS' \) -exec cp {} release-assets/ \;
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.dmg' -o -name 'SHA256SUMS' \) -exec cp {} release-assets/ \;
cp install.sh release-assets/
echo "--- Assets ---"
ls -lh release-assets/

View File

@ -1,6 +1,9 @@
name: Release Stable
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # stable tags only (no -beta suffix)
workflow_dispatch:
inputs:
version:
@ -33,11 +36,22 @@ jobs:
- name: Validate semver and Cargo.toml match
id: check
shell: bash
env:
INPUT_VERSION: ${{ inputs.version || '' }}
REF_NAME: ${{ github.ref_name }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
input_version="${{ inputs.version }}"
cargo_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
# Resolve version from tag push or manual input
if [[ "$EVENT_NAME" == "push" ]]; then
# Tag push: extract version from tag name (v0.5.9 -> 0.5.9)
input_version="${REF_NAME#v}"
else
input_version="$INPUT_VERSION"
fi
if [[ ! "$input_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Version must be semver (X.Y.Z). Got: ${input_version}"
exit 1
@ -49,9 +63,13 @@ jobs:
fi
tag="v${input_version}"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "::error::Tag ${tag} already exists."
exit 1
# Only check tag existence for manual dispatch (tag push means it already exists)
if [[ "$EVENT_NAME" != "push" ]]; then
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "::error::Tag ${tag} already exists."
exit 1
fi
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
@ -255,9 +273,64 @@ jobs:
path: zeroclaw-${{ matrix.target }}.${{ matrix.ext }}
retention-days: 14
build-desktop:
name: Build Desktop App (macOS Universal)
needs: [validate]
runs-on: macos-14
timeout-minutes: 40
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
prefix-key: macos-tauri
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Tauri CLI
run: cargo install tauri-cli --locked
- name: Sync Tauri version with Cargo.toml
shell: bash
run: |
VERSION=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
cd apps/tauri
if command -v jq >/dev/null 2>&1; then
jq --arg v "$VERSION" '.version = $v' tauri.conf.json > tmp.json && mv tmp.json tauri.conf.json
else
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" tauri.conf.json
fi
echo "Tauri version set to: $VERSION"
- name: Build Tauri app (universal binary)
working-directory: apps/tauri
run: cargo tauri build --target universal-apple-darwin
- name: Prepare desktop release assets
run: |
mkdir -p desktop-assets
find target -name '*.dmg' -exec cp {} desktop-assets/ZeroClaw.dmg \; 2>/dev/null || true
find target -name '*.app.tar.gz' -exec cp {} desktop-assets/ZeroClaw-macos.app.tar.gz \; 2>/dev/null || true
find target -name '*.app.tar.gz.sig' -exec cp {} desktop-assets/ZeroClaw-macos.app.tar.gz.sig \; 2>/dev/null || true
echo "--- Desktop assets ---"
ls -lh desktop-assets/
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: desktop-macos
path: desktop-assets/*
retention-days: 14
publish:
name: Publish Stable Release
needs: [validate, release-notes, build]
needs: [validate, release-notes, build, build-desktop]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -267,16 +340,21 @@ jobs:
pattern: zeroclaw-*
path: artifacts
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-macos
path: artifacts/desktop-macos
- name: Generate checksums
run: |
cd artifacts
find . -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
find . -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.dmg' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
cat SHA256SUMS
- name: Collect release assets
run: |
mkdir -p release-assets
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name 'SHA256SUMS' \) -exec cp {} release-assets/ \;
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.dmg' -o -name 'SHA256SUMS' \) -exec cp {} release-assets/ \;
cp install.sh release-assets/
echo "--- Assets ---"
ls -lh release-assets/
@ -286,6 +364,14 @@ jobs:
NOTES: ${{ needs.release-notes.outputs.notes }}
run: printf '%s\n' "$NOTES" > release-notes.md
- name: Create tag if manual dispatch
if: github.event_name == 'workflow_dispatch'
env:
TAG: ${{ needs.validate.outputs.tag }}
run: |
git tag -a "$TAG" -m "zeroclaw $TAG"
git push origin "$TAG"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
@ -323,6 +409,21 @@ jobs:
- name: Clean web build artifacts
run: rm -rf web/node_modules web/src web/package.json web/package-lock.json web/tsconfig*.json web/vite.config.ts web/index.html
- name: Publish aardvark-sys to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
OUTPUT=$(cargo publish --locked --allow-dirty --no-verify -p aardvark-sys 2>&1) && exit 0
echo "$OUTPUT"
if echo "$OUTPUT" | grep -q 'already exists'; then
echo "::notice::aardvark-sys already on crates.io — skipping"
exit 0
fi
exit 1
- name: Wait for aardvark-sys to index
run: sleep 15
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
@ -446,6 +547,16 @@ jobs:
dry_run: false
secrets: inherit
homebrew:
name: Update Homebrew Core
needs: [validate, publish]
if: ${{ !cancelled() && needs.publish.result == 'success' }}
uses: ./.github/workflows/pub-homebrew-core.yml
with:
release_tag: ${{ needs.validate.outputs.tag }}
dry_run: false
secrets: inherit
# ── Post-publish: tweet after release + website are live ──────────────
# Docker push can be slow; don't let it block the tweet.
tweet:

View File

@ -1 +0,0 @@
CLAUDE.md

92
AGENTS.md Normal file
View File

@ -0,0 +1,92 @@
# AGENTS.md — ZeroClaw
Cross-tool agent instructions for any AI coding assistant working on this repository.
## Commands
```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
```
Full pre-PR validation (recommended):
```bash
./dev/ci.sh all
```
Docs-only changes: run markdown lint and link-integrity checks. If touching bootstrap scripts: `bash -n install.sh`.
## Project Snapshot
ZeroClaw is a Rust-first autonomous agent runtime optimized for performance, efficiency, stability, extensibility, sustainability, and security.
Core architecture is trait-driven and modular. Extend by implementing traits and registering in factory modules.
Key extension points:
- `src/providers/traits.rs` (`Provider`)
- `src/channels/traits.rs` (`Channel`)
- `src/tools/traits.rs` (`Tool`)
- `src/memory/traits.rs` (`Memory`)
- `src/observability/traits.rs` (`Observer`)
- `src/runtime/traits.rs` (`RuntimeAdapter`)
- `src/peripherals/traits.rs` (`Peripheral`) — hardware boards (STM32, RPi GPIO)
## Repository Map
- `src/main.rs` — CLI entrypoint and command routing
- `src/lib.rs` — module exports and shared command enums
- `src/config/` — schema + config loading/merging
- `src/agent/` — orchestration loop
- `src/gateway/` — webhook/gateway server
- `src/security/` — policy, pairing, secret store
- `src/memory/` — markdown/sqlite memory backends + embeddings/vector merge
- `src/providers/` — model providers and resilient wrapper
- `src/channels/` — Telegram/Discord/Slack/etc channels
- `src/tools/` — tool execution surface (shell, file, memory, browser)
- `src/peripherals/` — hardware peripherals (STM32, RPi GPIO)
- `src/runtime/` — runtime adapters (currently native)
- `docs/` — topic-based documentation (setup-guides, reference, ops, security, hardware, contributing, maintainers)
- `.github/` — CI, templates, automation workflows
## Risk Tiers
- **Low risk**: docs/chore/tests-only changes
- **Medium risk**: most `src/**` behavior changes without boundary/security impact
- **High risk**: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**`, access-control boundaries
When uncertain, classify as higher risk.
## Workflow
1. **Read before write** — inspect existing module, factory wiring, and adjacent tests before editing.
2. **One concern per PR** — avoid mixed feature+refactor+infra patches.
3. **Implement minimal patch** — no speculative abstractions, no config keys without a concrete use case.
4. **Validate by risk tier** — docs-only: lightweight checks. Code changes: full relevant checks.
5. **Document impact** — update PR notes for behavior, risk, side effects, and rollback.
6. **Queue hygiene** — stacked PR: declare `Depends on #...`. Replacing old PR: declare `Supersedes #...`.
Branch/commit/PR rules:
- Work from a non-`master` branch. Open a PR to `master`; do not push directly.
- Use conventional commit titles. Prefer small PRs (`size: XS/S/M`).
- Follow `.github/pull_request_template.md` fully.
- Never commit secrets, personal data, or real identity information (see `@docs/contributing/pr-discipline.md`).
## Anti-Patterns
- Do not add heavy dependencies for minor convenience.
- Do not silently weaken security policy or access constraints.
- Do not add speculative config/feature flags "just in case".
- Do not mix massive formatting-only changes with functional changes.
- Do not modify unrelated modules "while here".
- Do not bypass failing checks without explicit explanation.
- Do not hide behavior-changing side effects in refactor commits.
- Do not include personal identity or sensitive information in test data, examples, docs, or commits.
## Linked References
- `@docs/contributing/change-playbooks.md` — adding providers, channels, tools, peripherals; security/gateway changes; architecture boundaries
- `@docs/contributing/pr-discipline.md` — privacy rules, superseded-PR attribution/templates, handoff template
- `@docs/contributing/docs-contract.md` — docs system contract, i18n rules, locale parity

View File

@ -1,90 +1,16 @@
# CLAUDE.md — ZeroClaw
# CLAUDE.md — ZeroClaw (Claude Code)
## Commands
> **Shared instructions live in [`AGENTS.md`](./AGENTS.md).**
> This file contains only Claude Code-specific directives.
```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
```
## Claude Code Settings
Full pre-PR validation (recommended):
Claude Code should read and follow all instructions in `AGENTS.md` at the repository root for project conventions, commands, risk tiers, workflow rules, and anti-patterns.
```bash
./dev/ci.sh all
```
## Hooks
Docs-only changes: run markdown lint and link-integrity checks. If touching bootstrap scripts: `bash -n install.sh`.
_No custom hooks defined yet._
## Project Snapshot
## Slash Commands
ZeroClaw is a Rust-first autonomous agent runtime optimized for performance, efficiency, stability, extensibility, sustainability, and security.
Core architecture is trait-driven and modular. Extend by implementing traits and registering in factory modules.
Key extension points:
- `src/providers/traits.rs` (`Provider`)
- `src/channels/traits.rs` (`Channel`)
- `src/tools/traits.rs` (`Tool`)
- `src/memory/traits.rs` (`Memory`)
- `src/observability/traits.rs` (`Observer`)
- `src/runtime/traits.rs` (`RuntimeAdapter`)
- `src/peripherals/traits.rs` (`Peripheral`) — hardware boards (STM32, RPi GPIO)
## Repository Map
- `src/main.rs` — CLI entrypoint and command routing
- `src/lib.rs` — module exports and shared command enums
- `src/config/` — schema + config loading/merging
- `src/agent/` — orchestration loop
- `src/gateway/` — webhook/gateway server
- `src/security/` — policy, pairing, secret store
- `src/memory/` — markdown/sqlite memory backends + embeddings/vector merge
- `src/providers/` — model providers and resilient wrapper
- `src/channels/` — Telegram/Discord/Slack/etc channels
- `src/tools/` — tool execution surface (shell, file, memory, browser)
- `src/peripherals/` — hardware peripherals (STM32, RPi GPIO)
- `src/runtime/` — runtime adapters (currently native)
- `docs/` — topic-based documentation (setup-guides, reference, ops, security, hardware, contributing, maintainers)
- `.github/` — CI, templates, automation workflows
## Risk Tiers
- **Low risk**: docs/chore/tests-only changes
- **Medium risk**: most `src/**` behavior changes without boundary/security impact
- **High risk**: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**`, access-control boundaries
When uncertain, classify as higher risk.
## Workflow
1. **Read before write** — inspect existing module, factory wiring, and adjacent tests before editing.
2. **One concern per PR** — avoid mixed feature+refactor+infra patches.
3. **Implement minimal patch** — no speculative abstractions, no config keys without a concrete use case.
4. **Validate by risk tier** — docs-only: lightweight checks. Code changes: full relevant checks.
5. **Document impact** — update PR notes for behavior, risk, side effects, and rollback.
6. **Queue hygiene** — stacked PR: declare `Depends on #...`. Replacing old PR: declare `Supersedes #...`.
Branch/commit/PR rules:
- Work from a non-`master` branch. Open a PR to `master`; do not push directly.
- Use conventional commit titles. Prefer small PRs (`size: XS/S/M`).
- Follow `.github/pull_request_template.md` fully.
- Never commit secrets, personal data, or real identity information (see `@docs/contributing/pr-discipline.md`).
## Anti-Patterns
- Do not add heavy dependencies for minor convenience.
- Do not silently weaken security policy or access constraints.
- Do not add speculative config/feature flags "just in case".
- Do not mix massive formatting-only changes with functional changes.
- Do not modify unrelated modules "while here".
- Do not bypass failing checks without explicit explanation.
- Do not hide behavior-changing side effects in refactor commits.
- Do not include personal identity or sensitive information in test data, examples, docs, or commits.
## Linked References
- `@docs/contributing/change-playbooks.md` — adding providers, channels, tools, peripherals; security/gateway changes; architecture boundaries
- `@docs/contributing/pr-discipline.md` — privacy rules, superseded-PR attribution/templates, handoff template
- `@docs/contributing/docs-contract.md` — docs system contract, i18n rules, locale parity
_No custom slash commands defined yet._

3617
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
[workspace]
members = [".", "crates/robot-kit"]
members = [".", "crates/robot-kit", "crates/aardvark-sys", "apps/tauri"]
resolver = "2"
[package]
name = "zeroclawlabs"
version = "0.5.3"
version = "0.6.1"
edition = "2021"
authors = ["theonlyhennygod"]
license = "MIT OR Apache-2.0"
@ -89,10 +89,16 @@ indicatif = "0.18"
# Temp files (update pipeline rollback)
tempfile = "3.26"
# Zip extraction for ClawhHub / OpenClaw registry installers
zip = { version = "8.1", default-features = false, features = ["deflate"] }
# Error handling
anyhow = "1.0"
thiserror = "2.0"
# Aardvark I2C/SPI/GPIO USB adapter (Total Phase) — stub when SDK absent
aardvark-sys = { path = "crates/aardvark-sys", version = "0.1.0" }
# UUID generation
uuid = { version = "1.22", default-features = false, features = ["v4", "std"] }
@ -144,6 +150,7 @@ which = "8.0"
# WebSocket client channels (Discord/Lark/DingTalk/Nostr)
tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] }
tokio-socks = "0.5"
futures-util = { version = "0.3", default-features = false, features = ["sink"] }
nostr-sdk = { version = "0.44", default-features = false, features = ["nip04", "nip59"], optional = true }
regex = "1.10"
@ -191,7 +198,10 @@ probe-rs = { version = "0.31", optional = true }
pdf-extract = { version = "0.10", optional = true }
# WASM plugin runtime (extism)
extism = { version = "1.9", optional = true }
extism = { version = "1.20", optional = true }
# Cross-platform audio capture for voice wake word detection (optional, enable with --features voice-wake)
cpal = { version = "0.15", optional = true }
# Terminal QR rendering for WhatsApp Web pairing flow.
qrcode = { version = "0.14", optional = true }
@ -215,7 +225,7 @@ landlock = { version = "0.4", optional = true }
libc = "0.2"
[features]
default = ["observability-prometheus", "channel-nostr", "skill-creation"]
default = ["observability-prometheus", "channel-nostr", "channel-lark", "skill-creation"]
channel-nostr = ["dep:nostr-sdk"]
hardware = ["nusb", "tokio-serial"]
channel-matrix = ["dep:matrix-sdk"]
@ -244,8 +254,30 @@ rag-pdf = ["dep:pdf-extract"]
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"]
# voice-wake = Voice wake word detection via microphone (cpal)
voice-wake = ["dep:cpal"]
# WASM plugin system (extism-based)
plugins-wasm = ["dep:extism"]
# Meta-feature for CI: all features except those requiring system C libraries
# not available on standard CI runners (e.g., voice-wake needs libasound2-dev).
ci-all = [
"channel-nostr",
"hardware",
"channel-matrix",
"channel-lark",
"memory-postgres",
"observability-prometheus",
"observability-otel",
"peripheral-rpi",
"browser-native",
"sandbox-landlock",
"sandbox-bubblewrap",
"probe",
"rag-pdf",
"skill-creation",
"whatsapp-web",
"plugins-wasm",
]
[profile.release]
opt-level = "z" # Optimize for size

View File

@ -12,7 +12,7 @@ RUN npm run build
FROM rust:1.94-slim@sha256:da9dab7a6b8dd428e71718402e97207bb3e54167d37b5708616050b1e8f60ed6 AS builder
WORKDIR /app
ARG ZEROCLAW_CARGO_FEATURES="memory-postgres"
ARG ZEROCLAW_CARGO_FEATURES="memory-postgres,channel-lark"
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
@ -23,9 +23,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# 1. Copy manifests to cache dependencies
COPY Cargo.toml Cargo.lock ./
# 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
# Include every workspace member: Cargo.lock is generated for the full workspace.
# Previously we used sed to drop `crates/robot-kit`, which made the manifest disagree
# with the lockfile and caused `cargo --locked` to fail (Cargo refused to rewrite the lock).
COPY crates/robot-kit/ crates/robot-kit/
COPY crates/aardvark-sys/ crates/aardvark-sys/
# Create dummy targets declared in Cargo.toml so manifest parsing succeeds.
RUN mkdir -p src benches \
&& echo "fn main() {}" > src/main.rs \
@ -60,7 +62,7 @@ RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/regist
fi && \
cp target/release/zeroclaw /app/zeroclaw && \
strip /app/zeroclaw
RUN size=$(stat -c%s /app/zeroclaw 2>/dev/null || stat -f%z /app/zeroclaw) && \
RUN size=$(stat -c%s /app/zeroclaw) && \
if [ "$size" -lt 1000000 ]; then echo "ERROR: binary too small (${size} bytes), likely dummy build artifact" && exit 1; fi
# Prepare runtime directory structure and default config inline (no extra stage)
@ -77,6 +79,10 @@ RUN mkdir -p /zeroclaw-data/.zeroclaw /zeroclaw-data/workspace && \
'port = 42617' \
'host = "[::]"' \
'allow_public_bind = true' \
'' \
'[autonomy]' \
'level = "supervised"' \
'auto_approve = ["file_read", "file_write", "file_edit", "memory_recall", "memory_store", "web_search_tool", "web_fetch", "calculator", "glob_search", "content_search", "image_info", "weather", "git_operations"]' \
> /zeroclaw-data/.zeroclaw/config.toml && \
chown -R 65534:65534 /zeroclaw-data

View File

@ -27,7 +27,7 @@ RUN npm run build
FROM rust:1.94-bookworm AS builder
WORKDIR /app
ARG ZEROCLAW_CARGO_FEATURES="memory-postgres"
ARG ZEROCLAW_CARGO_FEATURES="memory-postgres,channel-lark"
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
@ -38,9 +38,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# 1. Copy manifests to cache dependencies
COPY Cargo.toml Cargo.lock ./
# 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
# Include every workspace member: Cargo.lock is generated for the full workspace.
# Previously we used sed to drop `crates/robot-kit`, which made the manifest disagree
# with the lockfile and caused `cargo --locked` to fail (Cargo refused to rewrite the lock).
COPY crates/robot-kit/ crates/robot-kit/
# Create dummy targets declared in Cargo.toml so manifest parsing succeeds.
RUN mkdir -p src benches \
&& echo "fn main() {}" > src/main.rs \
@ -71,7 +72,7 @@ RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/regist
fi && \
cp target/release/zeroclaw /app/zeroclaw && \
strip /app/zeroclaw
RUN size=$(stat -c%s /app/zeroclaw 2>/dev/null || stat -f%z /app/zeroclaw) && \
RUN size=$(stat -c%s /app/zeroclaw) && \
if [ "$size" -lt 1000000 ]; then echo "ERROR: binary too small (${size} bytes), likely dummy build artifact" && exit 1; fi
# Prepare runtime directory structure and default config inline (no extra stage)
@ -88,6 +89,10 @@ RUN mkdir -p /zeroclaw-data/.zeroclaw /zeroclaw-data/workspace && \
'port = 42617' \
'host = "[::]"' \
'allow_public_bind = true' \
'' \
'[autonomy]' \
'level = "supervised"' \
'auto_approve = ["file_read", "file_write", "file_edit", "memory_recall", "memory_store", "web_search_tool", "web_fetch", "calculator", "glob_search", "content_search", "image_info", "weather", "git_operations"]' \
> /zeroclaw-data/.zeroclaw/config.toml && \
chown -R 65534:65534 /zeroclaw-data

15
NOTICE
View File

@ -41,3 +41,18 @@ This project uses third-party libraries and components,
each licensed under their respective terms.
See Cargo.lock for a complete dependency list.
Verifiable Intent Specification
================================
The src/verifiable_intent/ module is a Rust-native reimplementation based on
the Verifiable Intent open specification and reference implementation:
Project: Verifiable Intent (VI)
Author: agent-intent
Source: https://github.com/agent-intent/verifiable-intent
License: Apache License, Version 2.0
This implementation follows the VI specification design (SD-JWT layered
credentials, constraint model, three-layer chain). No source code was copied
from the reference implementation.

View File

@ -324,47 +324,6 @@ ls -lh target/release/zeroclaw
- CI/CD: تجريبي (تلقائي عند الدفع) → مستقر (إرسال يدوي) → Docker، crates.io، Scoop، AUR، Homebrew، تغريدة.
- ملفات ثنائية مُعدة مسبقًا لـ Linux (x86_64، aarch64، armv7)، macOS (x86_64، aarch64)، Windows (x86_64).
## كيف يعمل (باختصار)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## التكوين

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 ওয়েব ড্যাশবোর্
- CI/CD: বেটা (পুশে অটো) → স্টেবল (ম্যানুয়াল ডিসপ্যাচ) → Docker, crates.io, Scoop, AUR, Homebrew, টুইট।
- Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64) এর জন্য প্রি-বিল্ট বাইনারি।
## এটি কিভাবে কাজ করে (সংক্ষিপ্ত)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## কনফিগারেশন

View File

@ -324,47 +324,6 @@ Webový panel React 19 + Vite 6 + Tailwind CSS 4 servírovaný přímo z Gateway
- CI/CD: beta (auto na push) → stable (ruční dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Předpřipravené binárky pro Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Jak to funguje (krátce)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfigurace

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 web-dashboard serveret direkte fra Gateway'en
- CI/CD: beta (auto on push) → stable (manual dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Forhaandsbyggede binaerer til Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Saadan virker det (kort)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfiguration

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 Web-Dashboard, direkt vom Gateway bereitgeste
- CI/CD: beta (automatisch bei Push) → stable (manueller Dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, Tweet.
- Vorgefertigte Binaries für Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Wie es funktioniert (kurz)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (Steuerungsebene) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web-Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Ratenbegrenzung │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfiguration

View File

@ -324,47 +324,6 @@ ls -lh target/release/zeroclaw
- CI/CD: beta (auto on push) → stable (manual dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Προκατασκευασμένα δυαδικά για Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Πώς λειτουργεί (σύντομα)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Ρύθμιση παραμέτρων

View File

@ -324,47 +324,6 @@ Panel web React 19 + Vite 6 + Tailwind CSS 4 servido directamente desde el Gatew
- CI/CD: beta (automático al hacer push) → stable (dispatch manual) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Binarios preconstruidos para Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Cómo funciona (resumen)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (plano de control) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Panel Web (React 19) │
│ REST API + WebSocket + SSE │
│ Emparejamiento + Limitación │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configuración

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 web-hallintapaneeli, jota tarjoillaan suoraan
- CI/CD: beta (auto on push) → stable (manual dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Valmiit binaarit Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Miten se toimii (lyhyesti)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Maaritykset

View File

@ -324,47 +324,6 @@ Tableau de bord web React 19 + Vite 6 + Tailwind CSS 4 servi directement depuis
- CI/CD : beta (automatique au push) → stable (dispatch manuel) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Binaires précompilés pour Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Comment ça fonctionne (résumé)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (plan de contrôle) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Tableau de bord (React 19) │
│ REST API + WebSocket + SSE │
│ Appairage + Limitation │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configuration

View File

@ -324,47 +324,6 @@ ls -lh target/release/zeroclaw
- CI/CD: בטא (אוטומטי בדחיפה) → יציב (שליחה ידנית) → Docker, crates.io, Scoop, AUR, Homebrew, ציוץ.
- בינאריים מוכנים מראש ל-Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## איך זה עובד (בקצרה)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## הגדרות

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 वेब डैशबोर्ड सीध
- CI/CD: बीटा (पुश पर ऑटो) → स्टेबल (मैनुअल डिस्पैच) → Docker, crates.io, Scoop, AUR, Homebrew, ट्वीट।
- Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64) के लिए प्री-बिल्ट बाइनरी।
## यह कैसे काम करता है (संक्षिप्त)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## कॉन्फ़िगरेशन

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 webes vezerlopult, amelyet kozvetlenul a Gate
- CI/CD: beta (auto on push) → stable (manual dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Elore elkeszitett binarisok Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64) rendszerekhez.
## Hogyan mukodik (roviden)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfiguracio

View File

@ -324,47 +324,6 @@ Dasbor web React 19 + Vite 6 + Tailwind CSS 4 yang disajikan langsung dari Gatew
- CI/CD: beta (otomatis saat push) → stable (dispatch manual) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Biner pre-built untuk Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Cara kerjanya (singkat)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfigurasi

View File

@ -324,47 +324,6 @@ Dashboard web React 19 + Vite 6 + Tailwind CSS 4 servita direttamente dal Gatewa
- CI/CD: beta (automatico al push) → stable (dispatch manuale) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Binari precompilati per Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Come funziona (sintesi)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (piano di controllo) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Dashboard Web (React 19) │
│ REST API + WebSocket + SSE │
│ Accoppiamento + Limitazione │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configurazione

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 ウェブダッシュボード、Gatewayか
- CI/CDbetaプッシュ時自動→ stable手動ディスパッチ→ Docker、crates.io、Scoop、AUR、Homebrew、tweet。
- プリビルドバイナリLinuxx86_64、aarch64、armv7、macOSx86_64、aarch64、Windowsx86_64
## 仕組み(概要)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## 設定

View File

@ -324,47 +324,6 @@ Gateway에서 직접 제공하는 React 19 + Vite 6 + Tailwind CSS 4 웹 대시
- CI/CD: beta (push 시 자동) → stable (수동 디스패치) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64)용 사전 빌드 바이너리.
## 작동 방식 (요약)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## 구성

View File

@ -300,7 +300,7 @@ React 19 + Vite 6 + Tailwind CSS 4 web dashboard served directly from the Gatewa
- **Core:** shell, file read/write/edit, git operations, glob search, content search
- **Web:** browser control, web fetch, web search, screenshot, image info, PDF read
- **Integrations:** Jira, Notion, Google Workspace, Microsoft 365, LinkedIn, Composio, Pushover
- **Integrations:** Jira, Notion, Google Workspace, Microsoft 365, LinkedIn, Composio, Pushover, Weather (wttr.in)
- **MCP:** Model Context Protocol tool wrapper + deferred tool sets
- **Scheduling:** cron add/remove/update/run, schedule tool
- **Memory:** recall, store, forget, knowledge, project intel
@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 web dashboard served directly from the Gatewa
- CI/CD: beta (auto on push) → stable (manual dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Pre-built binaries for Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## How it works (short)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configuration

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 nettbasert dashbord servert direkte fra Gatew
- CI/CD: beta (auto pa push) -> stabil (manuell utsendelse) -> Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Forhandsbygde binarfiler for Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Slik fungerer det (kort)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (kontrollplan) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Nettbasert dashbord (React 19)│
│ REST API + WebSocket + SSE │
│ Paring + Hastighetsbegrensning│
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Sloyfe │ │Planleg.│ │ Sverm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Leveran.│ │Verktoy │ │ Minne │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Sikker- │ │Periferiutst│
│ het │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfigurasjon

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 webdashboard geserveerd direct vanuit de Gate
- CI/CD: beta (auto bij push) → stable (handmatige dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Voorgebouwde binaries voor Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Hoe het werkt (kort)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configuratie

View File

@ -324,47 +324,6 @@ Panel webowy React 19 + Vite 6 + Tailwind CSS 4 serwowany bezpośrednio z Gatewa
- CI/CD: beta (auto na push) → stable (ręczny dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Gotowe pliki binarne dla Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Jak to działa (w skrócie)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfiguracja

View File

@ -324,47 +324,6 @@ Painel web React 19 + Vite 6 + Tailwind CSS 4 servido diretamente pelo Gateway:
- CI/CD: beta (automático no push) → stable (dispatch manual) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Binários pré-construídos para Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Como funciona (resumo)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (plano de controle) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Painel Web (React 19) │
│ REST API + WebSocket + SSE │
│ Pareamento + Limitação │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configuração

View File

@ -324,47 +324,6 @@ Panou web React 19 + Vite 6 + Tailwind CSS 4 servit direct din Gateway:
- CI/CD: beta (automat la push) → stable (dispatch manual) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Binare pre-construite pentru Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Cum funcționează (pe scurt)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configurare

View File

@ -324,47 +324,6 @@ ls -lh target/release/zeroclaw
- CI/CD: бета (авто при push) → стабильный (ручной запуск) → Docker, crates.io, Scoop, AUR, Homebrew, твит.
- Предсобранные бинарные файлы для Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Как это работает (кратко)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Конфигурация

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 webbpanel serverad direkt från Gateway:
- CI/CD: beta (automatiskt vid push) → stable (manuell dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Förbyggda binärer för Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Hur det fungerar (kort)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (kontrollplan) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Webbpanel (React 19) │
│ REST API + WebSocket + SSE │
│ Parkoppling + Hastighetsbegränsning │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Konfiguration

View File

@ -324,47 +324,6 @@ Feature-gated: Matrix (`channel-matrix`), Lark (`channel-lark`), Nostr (`channel
- CI/CD: beta (อัตโนมัติเมื่อ push) → stable (dispatch แบบ manual) → Docker, crates.io, Scoop, AUR, Homebrew, tweet
- ไบนารี pre-built สำหรับ Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64)
## วิธีการทำงาน (สั้น)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## การกำหนดค่า

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 web dashboard na direktang inihahatid mula sa
- CI/CD: beta (auto sa push) → stable (manual dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Pre-built binaries para sa Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Paano gumagana (maikli)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Configuration

View File

@ -324,47 +324,6 @@ Gateway'den doğrudan sunulan React 19 + Vite 6 + Tailwind CSS 4 web paneli:
- CI/CD: beta (push'ta otomatik) → stable (manuel dispatch) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64) için önceden derlenmiş ikili dosyalar.
## Nasıl çalışır (kısaca)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Yapılandırma

View File

@ -324,47 +324,6 @@ ls -lh target/release/zeroclaw
- CI/CD: beta (автоматично при push) → stable (ручний запуск) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Попередньо зібрані бінарні файли для Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Як це працює (коротко)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Конфігурація

View File

@ -324,47 +324,6 @@ Gateway سے براہ راست فراہم کردہ React 19 + Vite 6 + Tailwind
- CI/CD: beta (push پر خودکار) → stable (دستی dispatch) → Docker، crates.io، Scoop، AUR، Homebrew، tweet۔
- Linux (x86_64، aarch64، armv7)، macOS (x86_64، aarch64)، Windows (x86_64) کے لیے پری بلٹ بائنریز۔
## یہ کیسے کام کرتا ہے (مختصر)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## کنفیگریشن

View File

@ -324,47 +324,6 @@ Bảng điều khiển web React 19 + Vite 6 + Tailwind CSS 4 được phục v
- CI/CD: beta (tự động khi push) → stable (dispatch thủ công) → Docker, crates.io, Scoop, AUR, Homebrew, tweet.
- Binary dựng sẵn cho Linux (x86_64, aarch64, armv7), macOS (x86_64, aarch64), Windows (x86_64).
## Cách hoạt động (tóm tắt)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## Cấu hình

View File

@ -324,47 +324,6 @@ React 19 + Vite 6 + Tailwind CSS 4 网页仪表板直接从 Gateway 提供:
- CI/CDbeta推送时自动→ stable手动触发→ Docker、crates.io、Scoop、AUR、Homebrew、tweet。
- 预构建二进制文件支持 Linuxx86_64、aarch64、armv7、macOSx86_64、aarch64、Windowsx86_64
## 工作原理(简述)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Matrix / IRC / Email
Bluesky / Nostr / Mattermost / DingTalk / Lark / QQ / Reddit / MQTT / WebSocket
┌───────────────────────────────┐
│ Gateway │
│ (control plane) │
│ http://127.0.0.1:42617 │
├───────────────────────────────┤
│ Web Dashboard (React 19) │
│ REST API + WebSocket + SSE │
│ Pairing + Rate Limiting │
└──────────────┬────────────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Agent │ │ Cron │ │ Hands │
│ Loop │ │Scheduler│ │ Swarm │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Provider│ │ Tools │ │ Memory │
│ (LLM) │ │ (70+) │ │(md/sql)│
└────────┘ └────────┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌────────────┐
│Security│ │ Peripherals│
│ Policy │ │(ESP32/STM32)│
└────────┘ └────────────┘
```
## 配置

29
apps/tauri/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "zeroclaw-desktop"
version = "0.1.0"
edition = "2021"
description = "ZeroClaw Desktop — Tauri-powered system tray app"
publish = false
[build-dependencies]
tauri-build = { version = "2.0", features = [] }
[dependencies]
tauri = { version = "2.0", features = ["tray-icon", "image-png"] }
tauri-plugin-shell = "2.0"
tauri-plugin-store = "2.0"
tauri-plugin-single-instance = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "1.50", features = ["rt-multi-thread", "macros", "sync", "time"] }
anyhow = "1.0"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSRunningApplication"] }
objc2-foundation = { version = "0.3", features = ["NSData"] }
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]

3
apps/tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build();
}

View File

@ -0,0 +1,14 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability set for ZeroClaw Desktop",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"store:allow-get",
"store:allow-set",
"store:allow-save",
"store:allow-load"
]
}

View File

@ -0,0 +1,14 @@
{
"identifier": "desktop",
"description": "Desktop-specific permissions for ZeroClaw",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"shell:allow-execute",
"store:allow-get",
"store:allow-set",
"store:allow-save",
"store:allow-load"
]
}

View File

@ -0,0 +1,8 @@
{
"identifier": "mobile",
"description": "Mobile-specific permissions for ZeroClaw",
"windows": ["main"],
"permissions": [
"core:default"
]
}

View File

View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 B

BIN
apps/tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

BIN
apps/tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
apps/tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" rx="16" fill="#DC322F"/>
<text x="64" y="80" font-size="64" font-family="monospace" font-weight="bold" fill="white" text-anchor="middle">Z</text>
</svg>

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

View File

@ -0,0 +1,17 @@
use crate::gateway_client::GatewayClient;
use crate::state::SharedState;
use tauri::State;
#[tauri::command]
pub async fn send_message(
state: State<'_, SharedState>,
message: String,
) -> Result<serde_json::Value, String> {
let s = state.read().await;
let client = GatewayClient::new(&s.gateway_url, s.token.as_deref());
drop(s);
client
.send_webhook_message(&message)
.await
.map_err(|e| e.to_string())
}

View File

@ -0,0 +1,11 @@
use crate::gateway_client::GatewayClient;
use crate::state::SharedState;
use tauri::State;
#[tauri::command]
pub async fn list_channels(state: State<'_, SharedState>) -> Result<serde_json::Value, String> {
let s = state.read().await;
let client = GatewayClient::new(&s.gateway_url, s.token.as_deref());
drop(s);
client.get_status().await.map_err(|e| e.to_string())
}

View File

@ -0,0 +1,19 @@
use crate::gateway_client::GatewayClient;
use crate::state::SharedState;
use tauri::State;
#[tauri::command]
pub async fn get_status(state: State<'_, SharedState>) -> Result<serde_json::Value, String> {
let s = state.read().await;
let client = GatewayClient::new(&s.gateway_url, s.token.as_deref());
drop(s);
client.get_status().await.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn get_health(state: State<'_, SharedState>) -> Result<bool, String> {
let s = state.read().await;
let client = GatewayClient::new(&s.gateway_url, s.token.as_deref());
drop(s);
client.get_health().await.map_err(|e| e.to_string())
}

View File

@ -0,0 +1,4 @@
pub mod agent;
pub mod channels;
pub mod gateway;
pub mod pairing;

View File

@ -0,0 +1,19 @@
use crate::gateway_client::GatewayClient;
use crate::state::SharedState;
use tauri::State;
#[tauri::command]
pub async fn initiate_pairing(state: State<'_, SharedState>) -> Result<serde_json::Value, String> {
let s = state.read().await;
let client = GatewayClient::new(&s.gateway_url, s.token.as_deref());
drop(s);
client.initiate_pairing().await.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn get_devices(state: State<'_, SharedState>) -> Result<serde_json::Value, String> {
let s = state.read().await;
let client = GatewayClient::new(&s.gateway_url, s.token.as_deref());
drop(s);
client.get_devices().await.map_err(|e| e.to_string())
}

View File

@ -0,0 +1,213 @@
//! HTTP client for communicating with the ZeroClaw gateway.
use anyhow::{Context, Result};
pub struct GatewayClient {
pub(crate) base_url: String,
pub(crate) token: Option<String>,
client: reqwest::Client,
}
impl GatewayClient {
pub fn new(base_url: &str, token: Option<&str>) -> Self {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_default();
Self {
base_url: base_url.to_string(),
token: token.map(String::from),
client,
}
}
pub(crate) fn auth_header(&self) -> Option<String> {
self.token.as_ref().map(|t| format!("Bearer {t}"))
}
pub async fn get_status(&self) -> Result<serde_json::Value> {
let mut req = self.client.get(format!("{}/api/status", self.base_url));
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
let resp = req.send().await.context("status request failed")?;
Ok(resp.json().await?)
}
pub async fn get_health(&self) -> Result<bool> {
match self
.client
.get(format!("{}/health", self.base_url))
.send()
.await
{
Ok(resp) => Ok(resp.status().is_success()),
Err(_) => Ok(false),
}
}
pub async fn get_devices(&self) -> Result<serde_json::Value> {
let mut req = self.client.get(format!("{}/api/devices", self.base_url));
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
let resp = req.send().await.context("devices request failed")?;
Ok(resp.json().await?)
}
pub async fn initiate_pairing(&self) -> Result<serde_json::Value> {
let mut req = self
.client
.post(format!("{}/api/pairing/initiate", self.base_url));
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
let resp = req.send().await.context("pairing request failed")?;
Ok(resp.json().await?)
}
/// Check whether the gateway requires pairing.
pub async fn requires_pairing(&self) -> Result<bool> {
let resp = self
.client
.get(format!("{}/health", self.base_url))
.send()
.await
.context("health request failed")?;
let body: serde_json::Value = resp.json().await?;
Ok(body["require_pairing"].as_bool().unwrap_or(false))
}
/// Request a new pairing code from the gateway (localhost-only admin endpoint).
pub async fn request_new_paircode(&self) -> Result<String> {
let resp = self
.client
.post(format!("{}/admin/paircode/new", self.base_url))
.send()
.await
.context("paircode request failed")?;
let body: serde_json::Value = resp.json().await?;
body["pairing_code"]
.as_str()
.map(String::from)
.context("no pairing_code in response")
}
/// Exchange a pairing code for a bearer token.
pub async fn pair_with_code(&self, code: &str) -> Result<String> {
let resp = self
.client
.post(format!("{}/pair", self.base_url))
.header("X-Pairing-Code", code)
.send()
.await
.context("pair request failed")?;
if !resp.status().is_success() {
anyhow::bail!("pair request returned {}", resp.status());
}
let body: serde_json::Value = resp.json().await?;
body["token"]
.as_str()
.map(String::from)
.context("no token in pair response")
}
/// Validate an existing token by calling a protected endpoint.
pub async fn validate_token(&self) -> Result<bool> {
let mut req = self.client.get(format!("{}/api/status", self.base_url));
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
match req.send().await {
Ok(resp) => Ok(resp.status().is_success()),
Err(_) => Ok(false),
}
}
/// Auto-pair with the gateway: request a new code and exchange it for a token.
pub async fn auto_pair(&self) -> Result<String> {
let code = self.request_new_paircode().await?;
self.pair_with_code(&code).await
}
pub async fn send_webhook_message(&self, message: &str) -> Result<serde_json::Value> {
let mut req = self
.client
.post(format!("{}/webhook", self.base_url))
.json(&serde_json::json!({ "message": message }));
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
let resp = req.send().await.context("webhook request failed")?;
Ok(resp.json().await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_creation_no_token() {
let client = GatewayClient::new("http://127.0.0.1:42617", None);
assert_eq!(client.base_url, "http://127.0.0.1:42617");
assert!(client.token.is_none());
assert!(client.auth_header().is_none());
}
#[test]
fn client_creation_with_token() {
let client = GatewayClient::new("http://localhost:8080", Some("test-token"));
assert_eq!(client.base_url, "http://localhost:8080");
assert_eq!(client.token.as_deref(), Some("test-token"));
assert_eq!(client.auth_header().unwrap(), "Bearer test-token");
}
#[test]
fn client_custom_url() {
let client = GatewayClient::new("https://zeroclaw.example.com:9999", None);
assert_eq!(client.base_url, "https://zeroclaw.example.com:9999");
}
#[test]
fn auth_header_format() {
let client = GatewayClient::new("http://localhost", Some("zc_abc123"));
assert_eq!(client.auth_header().unwrap(), "Bearer zc_abc123");
}
#[tokio::test]
async fn health_returns_false_for_unreachable_host() {
// Connect to a port that should not be listening.
let client = GatewayClient::new("http://127.0.0.1:1", None);
let result = client.get_health().await.unwrap();
assert!(!result, "health should be false for unreachable host");
}
#[tokio::test]
async fn status_fails_for_unreachable_host() {
let client = GatewayClient::new("http://127.0.0.1:1", None);
let result = client.get_status().await;
assert!(result.is_err(), "status should fail for unreachable host");
}
#[tokio::test]
async fn devices_fails_for_unreachable_host() {
let client = GatewayClient::new("http://127.0.0.1:1", None);
let result = client.get_devices().await;
assert!(result.is_err(), "devices should fail for unreachable host");
}
#[tokio::test]
async fn pairing_fails_for_unreachable_host() {
let client = GatewayClient::new("http://127.0.0.1:1", None);
let result = client.initiate_pairing().await;
assert!(result.is_err(), "pairing should fail for unreachable host");
}
#[tokio::test]
async fn webhook_fails_for_unreachable_host() {
let client = GatewayClient::new("http://127.0.0.1:1", None);
let result = client.send_webhook_message("hello").await;
assert!(result.is_err(), "webhook should fail for unreachable host");
}
}

40
apps/tauri/src/health.rs Normal file
View File

@ -0,0 +1,40 @@
//! Background health polling for the ZeroClaw gateway.
use crate::gateway_client::GatewayClient;
use crate::state::SharedState;
use crate::tray::icon;
use std::time::Duration;
use tauri::{AppHandle, Emitter, Runtime};
const POLL_INTERVAL: Duration = Duration::from_secs(5);
/// Spawn a background task that polls gateway health and updates state + tray.
pub fn spawn_health_poller<R: Runtime>(app: AppHandle<R>, state: SharedState) {
tauri::async_runtime::spawn(async move {
loop {
let (url, token) = {
let s = state.read().await;
(s.gateway_url.clone(), s.token.clone())
};
let client = GatewayClient::new(&url, token.as_deref());
let healthy = client.get_health().await.unwrap_or(false);
let (connected, agent_status) = {
let mut s = state.write().await;
s.connected = healthy;
(s.connected, s.agent_status)
};
// Update the tray icon and tooltip to reflect current state.
if let Some(tray) = app.tray_by_id("main") {
let _ = tray.set_icon(Some(icon::icon_for_state(connected, agent_status)));
let _ = tray.set_tooltip(Some(icon::tooltip_for_state(connected, agent_status)));
}
let _ = app.emit("zeroclaw://status-changed", healthy);
tokio::time::sleep(POLL_INTERVAL).await;
}
});
}

136
apps/tauri/src/lib.rs Normal file
View File

@ -0,0 +1,136 @@
//! ZeroClaw Desktop — Tauri application library.
pub mod commands;
pub mod gateway_client;
pub mod health;
pub mod state;
pub mod tray;
use gateway_client::GatewayClient;
use state::shared_state;
use tauri::{Manager, RunEvent};
/// Attempt to auto-pair with the gateway so the WebView has a valid token
/// before the React frontend mounts. Runs on localhost so the admin endpoints
/// are accessible without auth.
async fn auto_pair(state: &state::SharedState) -> Option<String> {
let url = {
let s = state.read().await;
s.gateway_url.clone()
};
let client = GatewayClient::new(&url, None);
// Check if gateway is reachable and requires pairing.
if !client.requires_pairing().await.unwrap_or(false) {
return None; // Pairing disabled — no token needed.
}
// Check if we already have a valid token in state.
{
let s = state.read().await;
if let Some(ref token) = s.token {
let authed = GatewayClient::new(&url, Some(token));
if authed.validate_token().await.unwrap_or(false) {
return Some(token.clone()); // Existing token is valid.
}
}
}
// No valid token — auto-pair by requesting a new code and exchanging it.
let client = GatewayClient::new(&url, None);
match client.auto_pair().await {
Ok(token) => {
let mut s = state.write().await;
s.token = Some(token.clone());
Some(token)
}
Err(_) => None, // Gateway may not be ready yet; health poller will retry.
}
}
/// Inject a bearer token into the WebView's localStorage so the React app
/// skips the pairing dialog. Uses Tauri's WebviewWindow scripting API.
fn inject_token_into_webview<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>, token: &str) {
let escaped = token.replace('\\', "\\\\").replace('\'', "\\'");
let script = format!("localStorage.setItem('zeroclaw_token', '{escaped}')");
// WebviewWindow scripting is the standard Tauri API for running JS in the WebView.
let _ = window.eval(&script);
}
/// Set the macOS dock icon programmatically so it shows even in dev builds
/// (which don't have a proper .app bundle).
#[cfg(target_os = "macos")]
fn set_dock_icon() {
use objc2::{AnyThread, MainThreadMarker};
use objc2_app_kit::NSApplication;
use objc2_app_kit::NSImage;
use objc2_foundation::NSData;
let icon_bytes = include_bytes!("../icons/128x128.png");
// Safety: setup() runs on the main thread in Tauri.
let mtm = unsafe { MainThreadMarker::new_unchecked() };
let data = NSData::with_bytes(icon_bytes);
if let Some(image) = NSImage::initWithData(NSImage::alloc(), &data) {
let app = NSApplication::sharedApplication(mtm);
unsafe { app.setApplicationIconImage(Some(&image)) };
}
}
/// Configure and run the Tauri application.
pub fn run() {
let shared = shared_state();
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// When a second instance launches, focus the existing window.
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}))
.manage(shared.clone())
.invoke_handler(tauri::generate_handler![
commands::gateway::get_status,
commands::gateway::get_health,
commands::channels::list_channels,
commands::pairing::initiate_pairing,
commands::pairing::get_devices,
commands::agent::send_message,
])
.setup(move |app| {
// Set macOS dock icon (needed for dev builds without .app bundle).
#[cfg(target_os = "macos")]
set_dock_icon();
// Set up the system tray.
let _ = tray::setup_tray(app);
// Auto-pair with gateway and inject token into the WebView.
let app_handle = app.handle().clone();
let pair_state = shared.clone();
tauri::async_runtime::spawn(async move {
if let Some(token) = auto_pair(&pair_state).await {
if let Some(window) = app_handle.get_webview_window("main") {
inject_token_into_webview(&window, &token);
}
}
});
// Start background health polling.
health::spawn_health_poller(app.handle().clone(), shared.clone());
Ok(())
})
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|_app, event| {
// Keep the app running in the background when all windows are closed.
// This is the standard pattern for menu bar / tray apps.
if let RunEvent::ExitRequested { api, .. } = event {
api.prevent_exit();
}
});
}

8
apps/tauri/src/main.rs Normal file
View File

@ -0,0 +1,8 @@
//! ZeroClaw Desktop — main entry point.
//!
//! Prevents an additional console window on Windows in release.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
zeroclaw_desktop::run();
}

6
apps/tauri/src/mobile.rs Normal file
View File

@ -0,0 +1,6 @@
//! Mobile entry point for ZeroClaw Desktop (iOS/Android).
#[tauri::mobile_entry_point]
fn main() {
zeroclaw_desktop::run();
}

99
apps/tauri/src/state.rs Normal file
View File

@ -0,0 +1,99 @@
//! Shared application state for Tauri.
use std::sync::Arc;
use tokio::sync::RwLock;
/// Agent status as reported by the gateway.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentStatus {
Idle,
Working,
Error,
}
/// Shared application state behind an `Arc<RwLock<_>>`.
#[derive(Debug, Clone)]
pub struct AppState {
pub gateway_url: String,
pub token: Option<String>,
pub connected: bool,
pub agent_status: AgentStatus,
}
impl Default for AppState {
fn default() -> Self {
Self {
gateway_url: "http://127.0.0.1:42617".to_string(),
token: None,
connected: false,
agent_status: AgentStatus::Idle,
}
}
}
/// Thread-safe wrapper around `AppState`.
pub type SharedState = Arc<RwLock<AppState>>;
/// Create the default shared state.
pub fn shared_state() -> SharedState {
Arc::new(RwLock::new(AppState::default()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_state() {
let state = AppState::default();
assert_eq!(state.gateway_url, "http://127.0.0.1:42617");
assert!(state.token.is_none());
assert!(!state.connected);
assert_eq!(state.agent_status, AgentStatus::Idle);
}
#[test]
fn shared_state_is_cloneable() {
let s1 = shared_state();
let s2 = s1.clone();
// Both references point to the same allocation.
assert!(Arc::ptr_eq(&s1, &s2));
}
#[tokio::test]
async fn shared_state_concurrent_read_write() {
let state = shared_state();
// Write from one handle.
{
let mut s = state.write().await;
s.connected = true;
s.agent_status = AgentStatus::Working;
s.token = Some("zc_test".to_string());
}
// Read from cloned handle.
let state2 = state.clone();
let s = state2.read().await;
assert!(s.connected);
assert_eq!(s.agent_status, AgentStatus::Working);
assert_eq!(s.token.as_deref(), Some("zc_test"));
}
#[test]
fn agent_status_serialization() {
assert_eq!(
serde_json::to_string(&AgentStatus::Idle).unwrap(),
"\"idle\""
);
assert_eq!(
serde_json::to_string(&AgentStatus::Working).unwrap(),
"\"working\""
);
assert_eq!(
serde_json::to_string(&AgentStatus::Error).unwrap(),
"\"error\""
);
}
}

View File

@ -0,0 +1,25 @@
//! Tray menu event handling.
use tauri::{menu::MenuEvent, AppHandle, Manager, Runtime};
pub fn handle_menu_event<R: Runtime>(app: &AppHandle<R>, event: MenuEvent) {
match event.id().as_ref() {
"show" => show_main_window(app, None),
"chat" => show_main_window(app, Some("/agent")),
"quit" => {
app.exit(0);
}
_ => {}
}
}
fn show_main_window<R: Runtime>(app: &AppHandle<R>, navigate_to: Option<&str>) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
if let Some(path) = navigate_to {
let script = format!("window.location.hash = '{path}'");
let _ = window.eval(&script);
}
}
}

105
apps/tauri/src/tray/icon.rs Normal file
View File

@ -0,0 +1,105 @@
//! Tray icon management — swap icon based on connection/agent status.
use crate::state::AgentStatus;
use tauri::image::Image;
/// Embedded tray icon PNGs (22x22, RGBA).
const ICON_IDLE: &[u8] = include_bytes!("../../icons/tray-idle.png");
const ICON_WORKING: &[u8] = include_bytes!("../../icons/tray-working.png");
const ICON_ERROR: &[u8] = include_bytes!("../../icons/tray-error.png");
const ICON_DISCONNECTED: &[u8] = include_bytes!("../../icons/tray-disconnected.png");
/// Select the appropriate tray icon for the current state.
pub fn icon_for_state(connected: bool, status: AgentStatus) -> Image<'static> {
let bytes: &[u8] = if !connected {
ICON_DISCONNECTED
} else {
match status {
AgentStatus::Idle => ICON_IDLE,
AgentStatus::Working => ICON_WORKING,
AgentStatus::Error => ICON_ERROR,
}
};
Image::from_bytes(bytes).expect("embedded tray icon is a valid PNG")
}
/// Tooltip text for the current state.
pub fn tooltip_for_state(connected: bool, status: AgentStatus) -> &'static str {
if !connected {
return "ZeroClaw — Disconnected";
}
match status {
AgentStatus::Idle => "ZeroClaw — Idle",
AgentStatus::Working => "ZeroClaw — Working",
AgentStatus::Error => "ZeroClaw — Error",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn icon_disconnected_when_not_connected() {
// Should not panic — icon bytes are valid PNGs.
let _img = icon_for_state(false, AgentStatus::Idle);
let _img = icon_for_state(false, AgentStatus::Working);
let _img = icon_for_state(false, AgentStatus::Error);
}
#[test]
fn icon_connected_variants() {
let _idle = icon_for_state(true, AgentStatus::Idle);
let _working = icon_for_state(true, AgentStatus::Working);
let _error = icon_for_state(true, AgentStatus::Error);
}
#[test]
fn tooltip_disconnected() {
assert_eq!(
tooltip_for_state(false, AgentStatus::Idle),
"ZeroClaw — Disconnected"
);
// Agent status is irrelevant when disconnected.
assert_eq!(
tooltip_for_state(false, AgentStatus::Working),
"ZeroClaw — Disconnected"
);
assert_eq!(
tooltip_for_state(false, AgentStatus::Error),
"ZeroClaw — Disconnected"
);
}
#[test]
fn tooltip_connected_variants() {
assert_eq!(
tooltip_for_state(true, AgentStatus::Idle),
"ZeroClaw — Idle"
);
assert_eq!(
tooltip_for_state(true, AgentStatus::Working),
"ZeroClaw — Working"
);
assert_eq!(
tooltip_for_state(true, AgentStatus::Error),
"ZeroClaw — Error"
);
}
#[test]
fn embedded_icons_are_valid_png() {
// Verify the PNG signature (first 8 bytes) of each embedded icon.
let png_sig: &[u8] = &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
assert!(ICON_IDLE.starts_with(png_sig), "idle icon not valid PNG");
assert!(
ICON_WORKING.starts_with(png_sig),
"working icon not valid PNG"
);
assert!(ICON_ERROR.starts_with(png_sig), "error icon not valid PNG");
assert!(
ICON_DISCONNECTED.starts_with(png_sig),
"disconnected icon not valid PNG"
);
}
}

View File

@ -0,0 +1,19 @@
//! Tray menu construction.
use tauri::{
menu::{Menu, MenuItemBuilder, PredefinedMenuItem},
App, Runtime,
};
pub fn create_tray_menu<R: Runtime>(app: &App<R>) -> Result<Menu<R>, tauri::Error> {
let show = MenuItemBuilder::with_id("show", "Show Dashboard").build(app)?;
let chat = MenuItemBuilder::with_id("chat", "Agent Chat").build(app)?;
let sep1 = PredefinedMenuItem::separator(app)?;
let status = MenuItemBuilder::with_id("status", "Status: Checking...")
.enabled(false)
.build(app)?;
let sep2 = PredefinedMenuItem::separator(app)?;
let quit = MenuItemBuilder::with_id("quit", "Quit ZeroClaw").build(app)?;
Menu::with_items(app, &[&show, &chat, &sep1, &status, &sep2, &quit])
}

View File

@ -0,0 +1,34 @@
//! System tray integration for ZeroClaw Desktop.
pub mod events;
pub mod icon;
pub mod menu;
use tauri::{
tray::{TrayIcon, TrayIconBuilder, TrayIconEvent},
App, Manager, Runtime,
};
/// Set up the system tray icon and menu.
pub fn setup_tray<R: Runtime>(app: &App<R>) -> Result<TrayIcon<R>, tauri::Error> {
let menu = menu::create_tray_menu(app)?;
TrayIconBuilder::with_id("main")
.tooltip("ZeroClaw — Disconnected")
.icon(icon::icon_for_state(false, crate::state::AgentStatus::Idle))
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(events::handle_menu_event)
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { button, .. } = event {
if button == tauri::tray::MouseButton::Left {
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
}
})
.build(app)
}

View File

@ -0,0 +1,35 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/config.schema.json",
"productName": "ZeroClaw",
"version": "0.6.1",
"identifier": "ai.zeroclawlabs.desktop",
"build": {
"devUrl": "http://127.0.0.1:42617/_app/",
"frontendDist": "http://127.0.0.1:42617/_app/"
},
"app": {
"windows": [
{
"title": "ZeroClaw",
"width": 1200,
"height": 800,
"resizable": true,
"fullscreen": false,
"visible": false
}
],
"security": {
"csp": "default-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; script-src 'self' 'unsafe-inline' http://127.0.0.1:*; style-src 'self' 'unsafe-inline' http://127.0.0.1:*; img-src 'self' http://127.0.0.1:* data:"
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@ -263,7 +263,7 @@ fn bench_memory_operations(c: &mut Criterion) {
c.bench_function("memory_recall_top10", |b| {
b.iter(|| {
rt.block_on(async {
mem.recall(black_box("zeroclaw agent"), 10, None)
mem.recall(black_box("zeroclaw agent"), 10, None, None, None)
.await
.unwrap()
})

View File

@ -0,0 +1,25 @@
[package]
name = "aardvark-sys"
version = "0.1.0"
edition = "2021"
authors = ["theonlyhennygod"]
license = "MIT OR Apache-2.0"
description = "Low-level bindings for the Total Phase Aardvark I2C/SPI/GPIO USB adapter"
repository = "https://github.com/zeroclaw-labs/zeroclaw"
# NOTE: This crate is the ONLY place in ZeroClaw where unsafe code is permitted.
# The rest of the workspace remains #![forbid(unsafe_code)].
#
# Stub implementation: the Total Phase SDK (aardvark.h + aardvark.so) is NOT
# yet committed. All AardvarkHandle methods return Err(AardvarkError::NotFound)
# at runtime. No unsafe code is needed for the stub.
#
# To enable real hardware (once SDK files are in vendor/):
# 1. Add `bindgen = "0.69"` to [build-dependencies]
# 2. Add `libc = "0.2"` to [dependencies]
# 3. Uncomment the build.rs bindgen call
# 4. Replace stub method bodies with FFI calls via mod bindings
[dependencies]
libloading = "0.8"
thiserror = "2.0"

View File

@ -0,0 +1,27 @@
//! Build script for aardvark-sys.
//!
//! # SDK present (real hardware)
//! When the Total Phase SDK files are in `vendor/`:
//! - Sets linker search path for aardvark.so
//! - Generates src/bindings.rs via bindgen
//!
//! # SDK absent (stub)
//! Does nothing. All AardvarkHandle methods return errors at runtime.
fn main() {
// Stub: SDK not yet in vendor/
// Uncomment and fill in when aardvark.h + aardvark.so are available:
//
// println!("cargo:rustc-link-search=native=crates/aardvark-sys/vendor");
// println!("cargo:rustc-link-lib=dylib=aardvark");
// println!("cargo:rerun-if-changed=vendor/aardvark.h");
//
// let bindings = bindgen::Builder::default()
// .header("vendor/aardvark.h")
// .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
// .generate()
// .expect("Unable to generate aardvark bindings");
// bindings
// .write_to_file("src/bindings.rs")
// .expect("Could not write bindings");
}

View File

@ -0,0 +1,475 @@
//! Bindings for the Total Phase Aardvark I2C/SPI/GPIO USB adapter.
//!
//! Uses [`libloading`] to load `aardvark.so` at runtime — the same pattern
//! the official Total Phase C stub (`aardvark.c`) uses internally.
//!
//! # Library search order
//!
//! 1. `ZEROCLAW_AARDVARK_LIB` environment variable (full path to `aardvark.so`)
//! 2. `<workspace>/crates/aardvark-sys/vendor/aardvark.so` (development default)
//! 3. `./aardvark.so` (next to the binary, for deployment)
//!
//! If none resolve, every method returns
//! [`Err(AardvarkError::LibraryNotFound)`](AardvarkError::LibraryNotFound).
//!
//! # Safety
//!
//! This crate is the **only** place in ZeroClaw where `unsafe` is permitted.
//! All `unsafe` is confined to `extern "C"` call sites inside this file.
//! The public API is fully safe Rust.
use std::path::PathBuf;
use std::sync::OnceLock;
use libloading::{Library, Symbol};
use thiserror::Error;
// ── Constants from aardvark.h ─────────────────────────────────────────────
/// Bit set on a port returned by `aa_find_devices` when that port is in use.
const AA_PORT_NOT_FREE: u16 = 0x8000;
/// Configure adapter for I2C + GPIO (I2C master mode, SPI disabled).
const AA_CONFIG_GPIO_I2C: i32 = 0x02;
/// Configure adapter for SPI + GPIO (SPI master mode, I2C disabled).
const AA_CONFIG_SPI_GPIO: i32 = 0x01;
/// No I2C flags (standard 7-bit addressing, normal stop condition).
const AA_I2C_NO_FLAGS: i32 = 0x00;
/// Enable both onboard I2C pullup resistors (hardware v2+ only).
const AA_I2C_PULLUP_BOTH: u8 = 0x03;
// ── Library loading ───────────────────────────────────────────────────────
static AARDVARK_LIB: OnceLock<Option<Library>> = OnceLock::new();
fn lib() -> Option<&'static Library> {
AARDVARK_LIB
.get_or_init(|| {
let candidates: Vec<PathBuf> = vec![
// 1. Explicit env-var override (full path)
std::env::var("ZEROCLAW_AARDVARK_LIB")
.ok()
.map(PathBuf::from)
.unwrap_or_default(),
// 2. Vendor directory shipped with this crate (dev default)
{
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("vendor/aardvark.so");
p
},
// 3. Next to the running binary (deployment)
std::env::current_exe()
.ok()
.and_then(|e| e.parent().map(|d| d.join("aardvark.so")))
.unwrap_or_default(),
// 4. Current working directory
PathBuf::from("aardvark.so"),
];
let mut tried_any = false;
for path in &candidates {
if path.as_os_str().is_empty() {
continue;
}
tried_any = true;
match unsafe { Library::new(path) } {
Ok(lib) => {
// Verify the .so exports aa_c_version (Total Phase version gate).
// The .so exports c_aa_* symbols (not aa_*); aa_c_version is the
// one non-prefixed symbol used to confirm library identity.
let version_ok = unsafe {
lib.get::<unsafe extern "C" fn() -> u32>(b"aa_c_version\0").is_ok()
};
if !version_ok {
eprintln!(
"[aardvark-sys] {} loaded but aa_c_version not found — \
not a valid Aardvark library, skipping",
path.display()
);
continue;
}
eprintln!("[aardvark-sys] loaded library from {}", path.display());
return Some(lib);
}
Err(e) => {
let msg = e.to_string();
// Surface architecture mismatch explicitly — the most common
// failure on Apple Silicon machines with an x86_64 SDK.
if msg.contains("incompatible architecture") || msg.contains("mach-o file") {
eprintln!(
"[aardvark-sys] ARCHITECTURE MISMATCH loading {}: {}\n\
[aardvark-sys] The vendored aardvark.so is x86_64 but this \
binary is {}.\n\
[aardvark-sys] Download the arm64 SDK from https://www.totalphase.com/downloads/ \
or build with --target x86_64-apple-darwin.",
path.display(),
msg,
std::env::consts::ARCH,
);
} else {
eprintln!(
"[aardvark-sys] could not load {}: {}",
path.display(),
msg
);
}
}
}
}
if !tried_any {
eprintln!("[aardvark-sys] no library candidates found; set ZEROCLAW_AARDVARK_LIB or place aardvark.so next to the binary");
}
None
})
.as_ref()
}
/// Errors returned by Aardvark hardware operations.
#[derive(Debug, Error)]
pub enum AardvarkError {
/// No Aardvark adapter found — adapter not plugged in.
#[error("Aardvark adapter not found — is it plugged in?")]
NotFound,
/// `aa_open` returned a non-positive handle.
#[error("Aardvark open failed (code {0})")]
OpenFailed(i32),
/// `aa_i2c_write` returned a negative status code.
#[error("I2C write failed (code {0})")]
I2cWriteFailed(i32),
/// `aa_i2c_read` returned a negative status code.
#[error("I2C read failed (code {0})")]
I2cReadFailed(i32),
/// `aa_spi_write` returned a negative status code.
#[error("SPI transfer failed (code {0})")]
SpiTransferFailed(i32),
/// GPIO operation returned a negative status code.
#[error("GPIO error (code {0})")]
GpioError(i32),
/// `aardvark.so` could not be found or loaded.
#[error("aardvark.so not found — set ZEROCLAW_AARDVARK_LIB or place it next to the binary")]
LibraryNotFound,
}
/// Convenience `Result` alias for this crate.
pub type Result<T> = std::result::Result<T, AardvarkError>;
// ── Handle ────────────────────────────────────────────────────────────────
/// Safe RAII handle over the Aardvark C library handle.
///
/// Automatically closes the adapter on `Drop`.
///
/// **Usage pattern:** open a fresh handle per command and let it drop at the
/// end of each operation (lazy-open / eager-close).
pub struct AardvarkHandle {
handle: i32,
}
impl AardvarkHandle {
// ── Lifecycle ─────────────────────────────────────────────────────────
/// Open the first available (free) Aardvark adapter.
pub fn open() -> Result<Self> {
let ports = Self::find_devices();
let port = ports.first().copied().ok_or(AardvarkError::NotFound)?;
Self::open_port(i32::from(port))
}
/// Open a specific Aardvark adapter by port index.
pub fn open_port(port: i32) -> Result<Self> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
let handle: i32 = unsafe {
let f: Symbol<unsafe extern "C" fn(i32) -> i32> = lib
.get(b"c_aa_open\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
f(port)
};
if handle <= 0 {
Err(AardvarkError::OpenFailed(handle))
} else {
Ok(Self { handle })
}
}
/// Return the port numbers of all **free** connected adapters.
///
/// Ports in-use by another process are filtered out.
/// Returns an empty `Vec` when `aardvark.so` cannot be loaded.
pub fn find_devices() -> Vec<u16> {
let Some(lib) = lib() else {
eprintln!("[aardvark-sys] find_devices: library not loaded");
return Vec::new();
};
let mut ports = [0u16; 16];
let n: i32 = unsafe {
let f: std::result::Result<Symbol<unsafe extern "C" fn(i32, *mut u16) -> i32>, _> =
lib.get(b"c_aa_find_devices\0");
match f {
Ok(f) => f(16, ports.as_mut_ptr()),
Err(e) => {
eprintln!("[aardvark-sys] find_devices: symbol lookup failed: {e}");
return Vec::new();
}
}
};
eprintln!(
"[aardvark-sys] find_devices: c_aa_find_devices returned {n}, ports={:?}",
&ports[..n.max(0) as usize]
);
if n <= 0 {
return Vec::new();
}
let free: Vec<u16> = ports[..n as usize]
.iter()
.filter(|&&p| (p & AA_PORT_NOT_FREE) == 0)
.copied()
.collect();
eprintln!("[aardvark-sys] find_devices: free ports={free:?}");
free
}
// ── I2C ───────────────────────────────────────────────────────────────
/// Enable I2C mode and set the bitrate (kHz).
pub fn i2c_enable(&self, bitrate_khz: u32) -> Result<()> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
unsafe {
let configure: Symbol<unsafe extern "C" fn(i32, i32) -> i32> = lib
.get(b"c_aa_configure\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
configure(self.handle, AA_CONFIG_GPIO_I2C);
let pullup: Symbol<unsafe extern "C" fn(i32, u8) -> i32> = lib
.get(b"c_aa_i2c_pullup\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
pullup(self.handle, AA_I2C_PULLUP_BOTH);
let bitrate: Symbol<unsafe extern "C" fn(i32, i32) -> i32> = lib
.get(b"c_aa_i2c_bitrate\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
bitrate(self.handle, bitrate_khz as i32);
}
Ok(())
}
/// Write `data` bytes to the I2C device at `addr`.
pub fn i2c_write(&self, addr: u8, data: &[u8]) -> Result<()> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
let ret: i32 = unsafe {
let f: Symbol<unsafe extern "C" fn(i32, u16, i32, u16, *const u8) -> i32> = lib
.get(b"c_aa_i2c_write\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
f(
self.handle,
u16::from(addr),
AA_I2C_NO_FLAGS,
data.len() as u16,
data.as_ptr(),
)
};
if ret < 0 {
Err(AardvarkError::I2cWriteFailed(ret))
} else {
Ok(())
}
}
/// Read `len` bytes from the I2C device at `addr`.
pub fn i2c_read(&self, addr: u8, len: usize) -> Result<Vec<u8>> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
let mut buf = vec![0u8; len];
let ret: i32 = unsafe {
let f: Symbol<unsafe extern "C" fn(i32, u16, i32, u16, *mut u8) -> i32> = lib
.get(b"c_aa_i2c_read\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
f(
self.handle,
u16::from(addr),
AA_I2C_NO_FLAGS,
len as u16,
buf.as_mut_ptr(),
)
};
if ret < 0 {
Err(AardvarkError::I2cReadFailed(ret))
} else {
Ok(buf)
}
}
/// Write then read — standard I2C register-read pattern.
pub fn i2c_write_read(&self, addr: u8, write_data: &[u8], read_len: usize) -> Result<Vec<u8>> {
self.i2c_write(addr, write_data)?;
self.i2c_read(addr, read_len)
}
/// Scan the I2C bus, returning addresses of all responding devices.
///
/// Probes `0x080x77` with a 1-byte read; returns addresses that ACK.
pub fn i2c_scan(&self) -> Vec<u8> {
let Some(lib) = lib() else {
return Vec::new();
};
let Ok(f): std::result::Result<
Symbol<unsafe extern "C" fn(i32, u16, i32, u16, *mut u8) -> i32>,
_,
> = (unsafe { lib.get(b"c_aa_i2c_read\0") }) else {
return Vec::new();
};
let mut found = Vec::new();
let mut buf = [0u8; 1];
for addr in 0x08u16..=0x77 {
let ret = unsafe { f(self.handle, addr, AA_I2C_NO_FLAGS, 1, buf.as_mut_ptr()) };
// ret > 0: bytes received → device ACKed
// ret == 0: NACK → no device at this address
// ret < 0: error code → skip
if ret > 0 {
found.push(addr as u8);
}
}
found
}
// ── SPI ───────────────────────────────────────────────────────────────
/// Enable SPI mode and set the bitrate (kHz).
pub fn spi_enable(&self, bitrate_khz: u32) -> Result<()> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
unsafe {
let configure: Symbol<unsafe extern "C" fn(i32, i32) -> i32> = lib
.get(b"c_aa_configure\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
configure(self.handle, AA_CONFIG_SPI_GPIO);
// SPI mode 0: polarity=rising/falling(0), phase=sample/setup(0), MSB first(0)
let spi_cfg: Symbol<unsafe extern "C" fn(i32, i32, i32, i32) -> i32> = lib
.get(b"c_aa_spi_configure\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
spi_cfg(self.handle, 0, 0, 0);
let bitrate: Symbol<unsafe extern "C" fn(i32, i32) -> i32> = lib
.get(b"c_aa_spi_bitrate\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
bitrate(self.handle, bitrate_khz as i32);
}
Ok(())
}
/// Full-duplex SPI transfer.
///
/// Sends `send` bytes; returns the simultaneously received bytes (same length).
pub fn spi_transfer(&self, send: &[u8]) -> Result<Vec<u8>> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
let mut recv = vec![0u8; send.len()];
// aa_spi_write(aardvark, out_num_bytes, data_out, in_num_bytes, data_in)
let ret: i32 = unsafe {
let f: Symbol<unsafe extern "C" fn(i32, u16, *const u8, u16, *mut u8) -> i32> = lib
.get(b"c_aa_spi_write\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
f(
self.handle,
send.len() as u16,
send.as_ptr(),
recv.len() as u16,
recv.as_mut_ptr(),
)
};
if ret < 0 {
Err(AardvarkError::SpiTransferFailed(ret))
} else {
Ok(recv)
}
}
// ── GPIO ──────────────────────────────────────────────────────────────
/// Set GPIO pin directions and output values.
///
/// `direction`: bitmask — `1` = output, `0` = input.
/// `value`: output state bitmask.
pub fn gpio_set(&self, direction: u8, value: u8) -> Result<()> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
unsafe {
let dir_f: Symbol<unsafe extern "C" fn(i32, u8) -> i32> = lib
.get(b"c_aa_gpio_direction\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
let d = dir_f(self.handle, direction);
if d < 0 {
return Err(AardvarkError::GpioError(d));
}
let set_f: Symbol<unsafe extern "C" fn(i32, u8) -> i32> =
lib.get(b"c_aa_gpio_set\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
let r = set_f(self.handle, value);
if r < 0 {
return Err(AardvarkError::GpioError(r));
}
}
Ok(())
}
/// Read the current GPIO pin states as a bitmask.
pub fn gpio_get(&self) -> Result<u8> {
let lib = lib().ok_or(AardvarkError::LibraryNotFound)?;
let ret: i32 = unsafe {
let f: Symbol<unsafe extern "C" fn(i32) -> i32> = lib
.get(b"c_aa_gpio_get\0")
.map_err(|_| AardvarkError::LibraryNotFound)?;
f(self.handle)
};
if ret < 0 {
Err(AardvarkError::GpioError(ret))
} else {
Ok(ret as u8)
}
}
}
impl Drop for AardvarkHandle {
fn drop(&mut self) {
if let Some(lib) = lib() {
unsafe {
if let Ok(f) = lib.get::<unsafe extern "C" fn(i32) -> i32>(b"c_aa_close\0") {
f(self.handle);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_devices_does_not_panic() {
// With no adapter plugged in, must return empty without panicking.
let _ = AardvarkHandle::find_devices();
}
#[test]
fn open_returns_error_or_ok_depending_on_hardware() {
// With hardware connected: open() succeeds (Ok).
// Without hardware: returns LibraryNotFound, NotFound, or OpenFailed — any Err is fine.
// Both outcomes are valid; the important thing is no panic.
let _ = AardvarkHandle::open();
}
#[test]
fn open_port_returns_error_when_no_hardware() {
// Port 99 doesn't exist — must return an error regardless of whether hardware is connected.
assert!(AardvarkHandle::open_port(99).is_err());
}
#[test]
fn error_display_messages_are_human_readable() {
assert!(AardvarkError::NotFound
.to_string()
.to_lowercase()
.contains("not found"));
assert!(AardvarkError::OpenFailed(-1).to_string().contains("-1"));
assert!(AardvarkError::I2cWriteFailed(-3)
.to_string()
.contains("I2C write"));
assert!(AardvarkError::SpiTransferFailed(-2)
.to_string()
.contains("SPI"));
assert!(AardvarkError::LibraryNotFound
.to_string()
.contains("aardvark.so"));
}
}

919
crates/aardvark-sys/vendor/aardvark.h vendored Normal file
View File

@ -0,0 +1,919 @@
/*=========================================================================
| Aardvark Interface Library
|--------------------------------------------------------------------------
| Copyright (c) 2003-2024 Total Phase, Inc.
| All rights reserved.
| www.totalphase.com
|
| Redistribution and use of this file in source and binary forms, with
| or without modification, are permitted provided that the following
| conditions are met:
|
| - Redistributions of source code must retain the above copyright
| notice, this list of conditions, and the following disclaimer.
|
| - Redistributions in binary form must reproduce the above copyright
| notice, this list of conditions, and the following disclaimer in the
| documentation or other materials provided with the distribution.
|
| - This file must only be used to interface with Total Phase products.
| The names of Total Phase and its contributors must not be used to
| endorse or promote products derived from this software.
|
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING BUT NOT
| LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
| FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT WILL THE
| COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
| INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
| BUT NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
| LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
| ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
| POSSIBILITY OF SUCH DAMAGE.
|--------------------------------------------------------------------------
| To access Total Phase Aardvark devices through the API:
|
| 1) Use one of the following shared objects:
| aardvark.so -- Linux or macOS shared object
| aardvark.dll -- Windows dynamic link library
|
| 2) Along with one of the following language modules:
| aardvark.c/h -- C/C++ API header file and interface module
| aardvark_py.py -- Python API
| aardvark.cs -- C# .NET source
| aardvark_net.dll -- Compiled .NET binding
| aardvark.bas -- Visual Basic 6 API
========================================================================*/
#ifndef __aardvark_h__
#define __aardvark_h__
#ifdef __cplusplus
extern "C" {
#endif
/*=========================================================================
| TYPEDEFS
========================================================================*/
#ifndef TOTALPHASE_DATA_TYPES
#define TOTALPHASE_DATA_TYPES
#ifndef _MSC_VER
/* C99-compliant compilers (GCC) */
#include <stdint.h>
typedef uint8_t u08;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef int8_t s08;
typedef int16_t s16;
typedef int32_t s32;
typedef int64_t s64;
#else
/* Microsoft compilers (Visual C++) */
typedef unsigned __int8 u08;
typedef unsigned __int16 u16;
typedef unsigned __int32 u32;
typedef unsigned __int64 u64;
typedef signed __int8 s08;
typedef signed __int16 s16;
typedef signed __int32 s32;
typedef signed __int64 s64;
#endif /* __MSC_VER */
typedef float f32;
typedef double f64;
#endif /* TOTALPHASE_DATA_TYPES */
/*=========================================================================
| DEBUG
========================================================================*/
/* Set the following macro to '1' for debugging */
#define AA_DEBUG 0
/*=========================================================================
| VERSION
========================================================================*/
#define AA_HEADER_VERSION 0x0600 /* v6.00 */
/*=========================================================================
| STATUS CODES
========================================================================*/
/*
* All API functions return an integer which is the result of the
* transaction, or a status code if negative. The status codes are
* defined as follows:
*/
enum AardvarkStatus {
/* General codes (0 to -99) */
AA_OK = 0,
AA_UNABLE_TO_LOAD_LIBRARY = -1,
AA_UNABLE_TO_LOAD_DRIVER = -2,
AA_UNABLE_TO_LOAD_FUNCTION = -3,
AA_INCOMPATIBLE_LIBRARY = -4,
AA_INCOMPATIBLE_DEVICE = -5,
AA_COMMUNICATION_ERROR = -6,
AA_UNABLE_TO_OPEN = -7,
AA_UNABLE_TO_CLOSE = -8,
AA_INVALID_HANDLE = -9,
AA_CONFIG_ERROR = -10,
/* I2C codes (-100 to -199) */
AA_I2C_NOT_AVAILABLE = -100,
AA_I2C_NOT_ENABLED = -101,
AA_I2C_READ_ERROR = -102,
AA_I2C_WRITE_ERROR = -103,
AA_I2C_SLAVE_BAD_CONFIG = -104,
AA_I2C_SLAVE_READ_ERROR = -105,
AA_I2C_SLAVE_TIMEOUT = -106,
AA_I2C_DROPPED_EXCESS_BYTES = -107,
AA_I2C_BUS_ALREADY_FREE = -108,
/* SPI codes (-200 to -299) */
AA_SPI_NOT_AVAILABLE = -200,
AA_SPI_NOT_ENABLED = -201,
AA_SPI_WRITE_ERROR = -202,
AA_SPI_SLAVE_READ_ERROR = -203,
AA_SPI_SLAVE_TIMEOUT = -204,
AA_SPI_DROPPED_EXCESS_BYTES = -205,
/* GPIO codes (-400 to -499) */
AA_GPIO_NOT_AVAILABLE = -400
};
#ifndef __cplusplus
typedef enum AardvarkStatus AardvarkStatus;
#endif
/*=========================================================================
| GENERAL TYPE DEFINITIONS
========================================================================*/
/* Aardvark handle type definition */
typedef int Aardvark;
/*
* Deprecated type definitions.
*
* These are only for use with legacy code and
* should not be used for new development.
*/
typedef u08 aa_u08;
typedef u16 aa_u16;
typedef u32 aa_u32;
typedef s08 aa_s08;
typedef s16 aa_s16;
typedef s32 aa_s32;
/*
* Aardvark version matrix.
*
* This matrix describes the various version dependencies
* of Aardvark components. It can be used to determine
* which component caused an incompatibility error.
*
* All version numbers are of the format:
* (major << 8) | minor
*
* ex. v1.20 would be encoded as: 0x0114
*/
struct AardvarkVersion {
/* Software, firmware, and hardware versions. */
u16 software;
u16 firmware;
u16 hardware;
/* Firmware requires that software must be >= this version. */
u16 sw_req_by_fw;
/* Software requires that firmware must be >= this version. */
u16 fw_req_by_sw;
/* Software requires that the API interface must be >= this version. */
u16 api_req_by_sw;
};
#ifndef __cplusplus
typedef struct AardvarkVersion AardvarkVersion;
#endif
/*=========================================================================
| GENERAL API
========================================================================*/
/*
* Get a list of ports to which Aardvark devices are attached.
*
* nelem = maximum number of elements to return
* devices = array into which the port numbers are returned
*
* Each element of the array is written with the port number.
* Devices that are in-use are ORed with AA_PORT_NOT_FREE (0x8000).
*
* ex. devices are attached to ports 0, 1, 2
* ports 0 and 2 are available, and port 1 is in-use.
* array => 0x0000, 0x8001, 0x0002
*
* If the array is NULL, it is not filled with any values.
* If there are more devices than the array size, only the
* first nmemb port numbers will be written into the array.
*
* Returns the number of devices found, regardless of the
* array size.
*/
#define AA_PORT_NOT_FREE 0x8000
int aa_find_devices (
int num_devices,
u16 * devices
);
/*
* Get a list of ports to which Aardvark devices are attached.
*
* This function is the same as aa_find_devices() except that
* it returns the unique IDs of each Aardvark device. The IDs
* are guaranteed to be non-zero if valid.
*
* The IDs are the unsigned integer representation of the 10-digit
* serial numbers.
*/
int aa_find_devices_ext (
int num_devices,
u16 * devices,
int num_ids,
u32 * unique_ids
);
/*
* Open the Aardvark port.
*
* The port number is a zero-indexed integer.
*
* The port number is the same as that obtained from the
* aa_find_devices() function above.
*
* Returns an Aardvark handle, which is guaranteed to be
* greater than zero if it is valid.
*
* This function is recommended for use in simple applications
* where extended information is not required. For more complex
* applications, the use of aa_open_ext() is recommended.
*/
Aardvark aa_open (
int port_number
);
/*
* Open the Aardvark port, returning extended information
* in the supplied structure. Behavior is otherwise identical
* to aa_open() above. If 0 is passed as the pointer to the
* structure, this function is exactly equivalent to aa_open().
*
* The structure is zeroed before the open is attempted.
* It is filled with whatever information is available.
*
* For example, if the firmware version is not filled, then
* the device could not be queried for its version number.
*
* This function is recommended for use in complex applications
* where extended information is required. For more simple
* applications, the use of aa_open() is recommended.
*/
struct AardvarkExt {
/* Version matrix */
AardvarkVersion version;
/* Features of this device. */
int features;
};
#ifndef __cplusplus
typedef struct AardvarkExt AardvarkExt;
#endif
Aardvark aa_open_ext (
int port_number,
AardvarkExt * aa_ext
);
/* Close the Aardvark port. */
int aa_close (
Aardvark aardvark
);
/*
* Return the port for this Aardvark handle.
*
* The port number is a zero-indexed integer.
*/
int aa_port (
Aardvark aardvark
);
/*
* Return the device features as a bit-mask of values, or
* an error code if the handle is not valid.
*/
#define AA_FEATURE_SPI 0x00000001
#define AA_FEATURE_I2C 0x00000002
#define AA_FEATURE_GPIO 0x00000008
int aa_features (
Aardvark aardvark
);
/*
* Return the unique ID for this Aardvark adapter.
* IDs are guaranteed to be non-zero if valid.
* The ID is the unsigned integer representation of the
* 10-digit serial number.
*/
u32 aa_unique_id (
Aardvark aardvark
);
/*
* Return the status string for the given status code.
* If the code is not valid or the library function cannot
* be loaded, return a NULL string.
*/
const char * aa_status_string (
int status
);
/*
* Enable logging to a file. The handle must be standard file
* descriptor. In C, a file descriptor can be obtained by using
* the ANSI C function "open" or by using the function "fileno"
* on a FILE* stream. A FILE* stream can be obtained using "fopen"
* or can correspond to the common "stdout" or "stderr" --
* available when including stdlib.h
*/
#define AA_LOG_STDOUT 1
#define AA_LOG_STDERR 2
int aa_log (
Aardvark aardvark,
int level,
int handle
);
/*
* Return the version matrix for the device attached to the
* given handle. If the handle is 0 or invalid, only the
* software and required api versions are set.
*/
int aa_version (
Aardvark aardvark,
AardvarkVersion * version
);
/*
* Configure the device by enabling/disabling I2C, SPI, and
* GPIO functions.
*/
enum AardvarkConfig {
AA_CONFIG_GPIO_ONLY = 0x00,
AA_CONFIG_SPI_GPIO = 0x01,
AA_CONFIG_GPIO_I2C = 0x02,
AA_CONFIG_SPI_I2C = 0x03,
AA_CONFIG_QUERY = 0x80
};
#ifndef __cplusplus
typedef enum AardvarkConfig AardvarkConfig;
#endif
#define AA_CONFIG_SPI_MASK 0x00000001
#define AA_CONFIG_I2C_MASK 0x00000002
int aa_configure (
Aardvark aardvark,
AardvarkConfig config
);
/*
* Configure the target power pins.
* This is only supported on hardware versions >= 2.00
*/
#define AA_TARGET_POWER_NONE 0x00
#define AA_TARGET_POWER_BOTH 0x03
#define AA_TARGET_POWER_QUERY 0x80
int aa_target_power (
Aardvark aardvark,
u08 power_mask
);
/*
* Sleep for the specified number of milliseconds
* Accuracy depends on the operating system scheduler
* Returns the number of milliseconds slept
*/
u32 aa_sleep_ms (
u32 milliseconds
);
/*=========================================================================
| ASYNC MESSAGE POLLING
========================================================================*/
/*
* Polling function to check if there are any asynchronous
* messages pending for processing. The function takes a timeout
* value in units of milliseconds. If the timeout is < 0, the
* function will block until data is received. If the timeout is 0,
* the function will perform a non-blocking check.
*/
#define AA_ASYNC_NO_DATA 0x00000000
#define AA_ASYNC_I2C_READ 0x00000001
#define AA_ASYNC_I2C_WRITE 0x00000002
#define AA_ASYNC_SPI 0x00000004
int aa_async_poll (
Aardvark aardvark,
int timeout
);
/*=========================================================================
| I2C API
========================================================================*/
/* Free the I2C bus. */
int aa_i2c_free_bus (
Aardvark aardvark
);
/*
* Set the I2C bit rate in kilohertz. If a zero is passed as the
* bitrate, the bitrate is unchanged and the current bitrate is
* returned.
*/
int aa_i2c_bitrate (
Aardvark aardvark,
int bitrate_khz
);
/*
* Set the bus lock timeout. If a zero is passed as the timeout,
* the timeout is unchanged and the current timeout is returned.
*/
int aa_i2c_bus_timeout (
Aardvark aardvark,
u16 timeout_ms
);
enum AardvarkI2cFlags {
AA_I2C_NO_FLAGS = 0x00,
AA_I2C_10_BIT_ADDR = 0x01,
AA_I2C_COMBINED_FMT = 0x02,
AA_I2C_NO_STOP = 0x04,
AA_I2C_SIZED_READ = 0x10,
AA_I2C_SIZED_READ_EXTRA1 = 0x20
};
#ifndef __cplusplus
typedef enum AardvarkI2cFlags AardvarkI2cFlags;
#endif
/* Read a stream of bytes from the I2C slave device. */
int aa_i2c_read (
Aardvark aardvark,
u16 slave_addr,
AardvarkI2cFlags flags,
u16 num_bytes,
u08 * data_in
);
enum AardvarkI2cStatus {
AA_I2C_STATUS_OK = 0,
AA_I2C_STATUS_BUS_ERROR = 1,
AA_I2C_STATUS_SLA_ACK = 2,
AA_I2C_STATUS_SLA_NACK = 3,
AA_I2C_STATUS_DATA_NACK = 4,
AA_I2C_STATUS_ARB_LOST = 5,
AA_I2C_STATUS_BUS_LOCKED = 6,
AA_I2C_STATUS_LAST_DATA_ACK = 7
};
#ifndef __cplusplus
typedef enum AardvarkI2cStatus AardvarkI2cStatus;
#endif
/*
* Read a stream of bytes from the I2C slave device.
* This API function returns the number of bytes read into
* the num_read variable. The return value of the function
* is a status code.
*/
int aa_i2c_read_ext (
Aardvark aardvark,
u16 slave_addr,
AardvarkI2cFlags flags,
u16 num_bytes,
u08 * data_in,
u16 * num_read
);
/* Write a stream of bytes to the I2C slave device. */
int aa_i2c_write (
Aardvark aardvark,
u16 slave_addr,
AardvarkI2cFlags flags,
u16 num_bytes,
const u08 * data_out
);
/*
* Write a stream of bytes to the I2C slave device.
* This API function returns the number of bytes written into
* the num_written variable. The return value of the function
* is a status code.
*/
int aa_i2c_write_ext (
Aardvark aardvark,
u16 slave_addr,
AardvarkI2cFlags flags,
u16 num_bytes,
const u08 * data_out,
u16 * num_written
);
/*
* Do an atomic write+read to an I2C slave device by first
* writing a stream of bytes to the I2C slave device and then
* reading a stream of bytes back from the same slave device.
* This API function returns the number of bytes written into
* the num_written variable and the number of bytes read into
* the num_read variable. The return value of the function is
* the status given as (read_status << 8) | (write_status).
*/
int aa_i2c_write_read (
Aardvark aardvark,
u16 slave_addr,
AardvarkI2cFlags flags,
u16 out_num_bytes,
const u08 * out_data,
u16 * num_written,
u16 in_num_bytes,
u08 * in_data,
u16 * num_read
);
/* Enable/Disable the Aardvark as an I2C slave device */
int aa_i2c_slave_enable (
Aardvark aardvark,
u08 addr,
u16 maxTxBytes,
u16 maxRxBytes
);
int aa_i2c_slave_disable (
Aardvark aardvark
);
/*
* Set the slave response in the event the Aardvark is put
* into slave mode and contacted by a Master.
*/
int aa_i2c_slave_set_response (
Aardvark aardvark,
u08 num_bytes,
const u08 * data_out
);
/*
* Return number of bytes written from a previous
* Aardvark->I2C_master transmission. Since the transmission is
* happening asynchronously with respect to the PC host
* software, there could be responses queued up from many
* previous write transactions.
*/
int aa_i2c_slave_write_stats (
Aardvark aardvark
);
/* Read the bytes from an I2C slave reception */
int aa_i2c_slave_read (
Aardvark aardvark,
u08 * addr,
u16 num_bytes,
u08 * data_in
);
/* Extended functions that return status code */
int aa_i2c_slave_write_stats_ext (
Aardvark aardvark,
u16 * num_written
);
int aa_i2c_slave_read_ext (
Aardvark aardvark,
u08 * addr,
u16 num_bytes,
u08 * data_in,
u16 * num_read
);
/*
* Configure the I2C pullup resistors.
* This is only supported on hardware versions >= 2.00
*/
#define AA_I2C_PULLUP_NONE 0x00
#define AA_I2C_PULLUP_BOTH 0x03
#define AA_I2C_PULLUP_QUERY 0x80
int aa_i2c_pullup (
Aardvark aardvark,
u08 pullup_mask
);
/*=========================================================================
| SPI API
========================================================================*/
/*
* Set the SPI bit rate in kilohertz. If a zero is passed as the
* bitrate, the bitrate is unchanged and the current bitrate is
* returned.
*/
int aa_spi_bitrate (
Aardvark aardvark,
int bitrate_khz
);
/*
* These configuration parameters specify how to clock the
* bits that are sent and received on the Aardvark SPI
* interface.
*
* The polarity option specifies which transition
* constitutes the leading edge and which transition is the
* falling edge. For example, AA_SPI_POL_RISING_FALLING
* would configure the SPI to idle the SCK clock line low.
* The clock would then transition low-to-high on the
* leading edge and high-to-low on the trailing edge.
*
* The phase option determines whether to sample or setup on
* the leading edge. For example, AA_SPI_PHASE_SAMPLE_SETUP
* would configure the SPI to sample on the leading edge and
* setup on the trailing edge.
*
* The bitorder option is used to indicate whether LSB or
* MSB is shifted first.
*
* See the diagrams in the Aardvark datasheet for
* more details.
*/
enum AardvarkSpiPolarity {
AA_SPI_POL_RISING_FALLING = 0,
AA_SPI_POL_FALLING_RISING = 1
};
#ifndef __cplusplus
typedef enum AardvarkSpiPolarity AardvarkSpiPolarity;
#endif
enum AardvarkSpiPhase {
AA_SPI_PHASE_SAMPLE_SETUP = 0,
AA_SPI_PHASE_SETUP_SAMPLE = 1
};
#ifndef __cplusplus
typedef enum AardvarkSpiPhase AardvarkSpiPhase;
#endif
enum AardvarkSpiBitorder {
AA_SPI_BITORDER_MSB = 0,
AA_SPI_BITORDER_LSB = 1
};
#ifndef __cplusplus
typedef enum AardvarkSpiBitorder AardvarkSpiBitorder;
#endif
/* Configure the SPI master or slave interface */
int aa_spi_configure (
Aardvark aardvark,
AardvarkSpiPolarity polarity,
AardvarkSpiPhase phase,
AardvarkSpiBitorder bitorder
);
/* Write a stream of bytes to the downstream SPI slave device. */
int aa_spi_write (
Aardvark aardvark,
u16 out_num_bytes,
const u08 * data_out,
u16 in_num_bytes,
u08 * data_in
);
/* Enable/Disable the Aardvark as an SPI slave device */
int aa_spi_slave_enable (
Aardvark aardvark
);
int aa_spi_slave_disable (
Aardvark aardvark
);
/*
* Set the slave response in the event the Aardvark is put
* into slave mode and contacted by a Master.
*/
int aa_spi_slave_set_response (
Aardvark aardvark,
u08 num_bytes,
const u08 * data_out
);
/* Read the bytes from an SPI slave reception */
int aa_spi_slave_read (
Aardvark aardvark,
u16 num_bytes,
u08 * data_in
);
/*
* Change the output polarity on the SS line.
*
* Note: When configured as an SPI slave, the Aardvark will
* always be setup with SS as active low. Hence this function
* only affects the SPI master functions on the Aardvark.
*/
enum AardvarkSpiSSPolarity {
AA_SPI_SS_ACTIVE_LOW = 0,
AA_SPI_SS_ACTIVE_HIGH = 1
};
#ifndef __cplusplus
typedef enum AardvarkSpiSSPolarity AardvarkSpiSSPolarity;
#endif
int aa_spi_master_ss_polarity (
Aardvark aardvark,
AardvarkSpiSSPolarity polarity
);
/*=========================================================================
| GPIO API
========================================================================*/
/*
* The following enumerated type maps the named lines on the
* Aardvark I2C/SPI line to bit positions in the GPIO API.
* All GPIO API functions will index these lines through an
* 8-bit masked value. Thus, each bit position in the mask
* can be referred back its corresponding line through the
* enumerated type.
*/
enum AardvarkGpioBits {
AA_GPIO_SCL = 0x01,
AA_GPIO_SDA = 0x02,
AA_GPIO_MISO = 0x04,
AA_GPIO_SCK = 0x08,
AA_GPIO_MOSI = 0x10,
AA_GPIO_SS = 0x20
};
#ifndef __cplusplus
typedef enum AardvarkGpioBits AardvarkGpioBits;
#endif
/*
* Configure the GPIO, specifying the direction of each bit.
*
* A call to this function will not change the value of the pullup
* mask in the Aardvark. This is illustrated by the following
* example:
* (1) Direction mask is first set to 0x00
* (2) Pullup is set to 0x01
* (3) Direction mask is set to 0x01
* (4) Direction mask is later set back to 0x00.
*
* The pullup will be active after (4).
*
* On Aardvark power-up, the default value of the direction
* mask is 0x00.
*/
#define AA_GPIO_DIR_INPUT 0
#define AA_GPIO_DIR_OUTPUT 1
int aa_gpio_direction (
Aardvark aardvark,
u08 direction_mask
);
/*
* Enable an internal pullup on any of the GPIO input lines.
*
* Note: If a line is configured as an output, the pullup bit
* for that line will be ignored, though that pullup bit will
* be cached in case the line is later configured as an input.
*
* By default the pullup mask is 0x00.
*/
#define AA_GPIO_PULLUP_OFF 0
#define AA_GPIO_PULLUP_ON 1
int aa_gpio_pullup (
Aardvark aardvark,
u08 pullup_mask
);
/*
* Read the current digital values on the GPIO input lines.
*
* The bits will be ordered as described by AA_GPIO_BITS. If a
* line is configured as an output, its corresponding bit
* position in the mask will be undefined.
*/
int aa_gpio_get (
Aardvark aardvark
);
/*
* Set the outputs on the GPIO lines.
*
* Note: If a line is configured as an input, it will not be
* affected by this call, but the output value for that line
* will be cached in the event that the line is later
* configured as an output.
*/
int aa_gpio_set (
Aardvark aardvark,
u08 value
);
/*
* Block until there is a change on the GPIO input lines.
* Pins configured as outputs will be ignored.
*
* The function will return either when a change has occurred or
* the timeout expires. The timeout, specified in millisecods, has
* a precision of ~16 ms. The maximum allowable timeout is
* approximately 4 seconds. If the timeout expires, this function
* will return the current state of the GPIO lines.
*
* This function will return immediately with the current value
* of the GPIO lines for the first invocation after any of the
* following functions are called: aa_configure,
* aa_gpio_direction, or aa_gpio_pullup.
*
* If the function aa_gpio_get is called before calling
* aa_gpio_change, aa_gpio_change will only register any changes
* from the value last returned by aa_gpio_get.
*/
int aa_gpio_change (
Aardvark aardvark,
u16 timeout
);
#ifdef __cplusplus
}
#endif
#endif /* __aardvark_h__ */

BIN
crates/aardvark-sys/vendor/aardvark.so vendored Normal file

Binary file not shown.

View File

@ -10,3 +10,22 @@ default_temperature = 0.7
port = 42617
host = "[::]"
allow_public_bind = true
# Cost tracking and budget enforcement configuration
# Enable to track API usage costs and enforce spending limits
[cost]
enabled = false
daily_limit_usd = 10.0
monthly_limit_usd = 100.0
warn_at_percent = 80
allow_override = false
# Per-model pricing (USD per 1M tokens)
# Uncomment and customize to override default pricing
# [cost.prices."anthropic/claude-sonnet-4-20250514"]
# input = 3.0
# output = 15.0
#
# [cost.prices."openai/gpt-4o"]
# input = 5.0
# output = 15.0

4
dist/aur/.SRCINFO vendored
View File

@ -1,6 +1,6 @@
pkgbase = zeroclaw
pkgdesc = Zero overhead. Zero compromise. 100% Rust. The fastest, smallest AI assistant.
pkgver = 0.5.3
pkgver = 0.5.9
pkgrel = 1
url = https://github.com/zeroclaw-labs/zeroclaw
arch = x86_64
@ -10,7 +10,7 @@ pkgbase = zeroclaw
makedepends = git
depends = gcc-libs
depends = openssl
source = zeroclaw-0.5.3.tar.gz::https://github.com/zeroclaw-labs/zeroclaw/archive/refs/tags/v0.5.3.tar.gz
source = zeroclaw-0.5.9.tar.gz::https://github.com/zeroclaw-labs/zeroclaw/archive/refs/tags/v0.5.9.tar.gz
sha256sums = SKIP
pkgname = zeroclaw

2
dist/aur/PKGBUILD vendored
View File

@ -1,6 +1,6 @@
# Maintainer: zeroclaw-labs <bot@zeroclaw.dev>
pkgname=zeroclaw
pkgver=0.5.3
pkgver=0.5.9
pkgrel=1
pkgdesc="Zero overhead. Zero compromise. 100% Rust. The fastest, smallest AI assistant."
arch=('x86_64')

View File

@ -1,11 +1,11 @@
{
"version": "0.5.3",
"version": "0.5.9",
"description": "Zero overhead. Zero compromise. 100% Rust. The fastest, smallest AI assistant.",
"homepage": "https://github.com/zeroclaw-labs/zeroclaw",
"license": "MIT|Apache-2.0",
"architecture": {
"64bit": {
"url": "https://github.com/zeroclaw-labs/zeroclaw/releases/download/v0.5.3/zeroclaw-x86_64-pc-windows-msvc.zip",
"url": "https://github.com/zeroclaw-labs/zeroclaw/releases/download/v0.5.9/zeroclaw-x86_64-pc-windows-msvc.zip",
"hash": "",
"bin": "zeroclaw.exe"
}

View File

@ -0,0 +1,325 @@
# Aardvark Integration — How It Works
A plain-language walkthrough of every piece and how they connect.
---
## The Big Picture
```
┌──────────────────────────────────────────────────────────────┐
│ STARTUP (boot) │
│ │
│ 1. Ask aardvark-sys: "any adapters plugged in?" │
│ 2. For each one found → register a device + transport │
│ 3. Load tools only if hardware was found │
└──────────────────────────────────────────┬───────────────────┘
┌──────────────────────▼──────────────────────┐
│ RUNTIME (agent loop) │
│ │
│ User: "scan i2c bus" │
│ → agent calls i2c_scan tool │
│ → tool builds a ZcCommand │
│ → AardvarkTransport sends to hardware │
│ → response flows back as text │
└──────────────────────────────────────────────┘
```
---
## Layer by Layer
### Layer 1 — `aardvark-sys` (the USB talker)
**File:** `crates/aardvark-sys/src/lib.rs`
This is the only layer that ever touches the raw C library.
Think of it as a thin translator: it turns C function calls into safe Rust.
**Algorithm:**
```
find_devices()
→ call aa_find_devices(16, buf) // ask C lib how many adapters
→ return Vec of port numbers // [0, 1, ...] one per adapter
open_port(port)
→ call aa_open(port) // open that specific adapter
→ if handle ≤ 0, return OpenFailed
→ else return AardvarkHandle{ _port: handle }
i2c_scan(handle)
→ for addr in 0x08..=0x77 // every valid 7-bit address
try aa_i2c_read(addr, 1 byte) // knock on the door
if ACK → add to list // device answered
→ return list of live addresses
i2c_read(handle, addr, len)
→ aa_i2c_read(addr, len bytes)
→ return bytes as Vec<u8>
i2c_write(handle, addr, data)
→ aa_i2c_write(addr, data)
spi_transfer(handle, bytes_to_send)
→ aa_spi_write(bytes) // full-duplex: sends + receives
→ return received bytes
gpio_set(handle, direction, value)
→ aa_gpio_direction(direction) // which pins are outputs
→ aa_gpio_put(value) // set output levels
gpio_get(handle)
→ aa_gpio_get() // read all pin levels as bitmask
Drop(handle)
→ aa_close(handle._port) // always close on drop
```
**In stub mode** (no SDK): every method returns `Err(NotFound)` immediately. `find_devices()` returns `[]`. Nothing crashes.
---
### Layer 2 — `AardvarkTransport` (the bridge)
**File:** `src/hardware/aardvark.rs`
The rest of ZeroClaw speaks a single language: `ZcCommand``ZcResponse`.
`AardvarkTransport` translates between that protocol and the aardvark-sys calls above.
**Algorithm:**
```
send(ZcCommand) → ZcResponse
extract command name from cmd.name
extract parameters from cmd.params (serde_json values)
match cmd.name:
"i2c_scan" → open handle → call i2c_scan()
→ format found addresses as hex list
→ return ZcResponse{ output: "0x48, 0x68" }
"i2c_read" → parse addr (hex string) + len (number)
→ open handle → i2c_enable(bitrate)
→ call i2c_read(addr, len)
→ format bytes as hex
→ return ZcResponse{ output: "0xAB 0xCD" }
"i2c_write" → parse addr + data bytes
→ open handle → i2c_write(addr, data)
→ return ZcResponse{ output: "ok" }
"spi_transfer" → parse bytes_hex string → decode to Vec<u8>
→ open handle → spi_enable(bitrate)
→ spi_transfer(bytes)
→ return received bytes as hex
"gpio_set" → parse direction + value bitmasks
→ open handle → gpio_set(dir, val)
→ return ZcResponse{ output: "ok" }
"gpio_get" → open handle → gpio_get()
→ return bitmask value as string
on any AardvarkError → return ZcResponse{ error: "..." }
```
**Key design choice — lazy open:** The handle is opened fresh for every command and dropped at the end. This means no held connection, no state to clean up, and no "is it still open?" logic anywhere.
---
### Layer 3 — Tools (what the agent calls)
**File:** `src/hardware/aardvark_tools.rs`
Each tool is a thin wrapper. It:
1. Validates the agent's JSON input
2. Resolves which physical device to use
3. Builds a `ZcCommand`
4. Calls `AardvarkTransport.send()`
5. Returns the result as text
```
I2cScanTool.call(args)
→ look up "device" in args (default: "aardvark0")
→ find that device in the registry
→ build ZcCommand{ name: "i2c_scan", params: {} }
→ send to AardvarkTransport
→ return "Found: 0x48, 0x68" (or "No devices found")
I2cReadTool.call(args)
→ require args["addr"] and args["len"]
→ build ZcCommand{ name: "i2c_read", params: {addr, len} }
→ send → return hex bytes
I2cWriteTool.call(args)
→ require args["addr"] and args["data"] (hex or array)
→ build ZcCommand{ name: "i2c_write", params: {addr, data} }
→ send → return "ok" or error
SpiTransferTool.call(args)
→ require args["bytes"] (hex string)
→ build ZcCommand{ name: "spi_transfer", params: {bytes} }
→ send → return received bytes
GpioAardvarkTool.call(args)
→ require args["direction"] + args["value"] (set)
OR no extra args (get)
→ build appropriate ZcCommand
→ send → return result
DatasheetTool.call(args)
→ action = args["action"]: "search" | "download" | "list" | "read"
→ "search": return a Google/vendor search URL for the device
→ "download": fetch PDF from args["url"] → save to ~/.zeroclaw/hardware/datasheets/
→ "list": scan the datasheets directory → return filenames
→ "read": open a saved PDF and return its text
```
---
### Layer 4 — Device Registry (the address book)
**File:** `src/hardware/device.rs`
The registry is a runtime map of every connected device.
Each entry stores: alias, kind, capabilities, transport handle.
```
register("aardvark", vid=0x2b76, ...)
→ DeviceKind::from_vid(0x2b76) → DeviceKind::Aardvark
→ DeviceRuntime::from_kind() → DeviceRuntime::Aardvark
→ assign alias "aardvark0" (then "aardvark1" for second, etc.)
→ store entry in HashMap
attach_transport("aardvark0", AardvarkTransport, capabilities{i2c,spi,gpio})
→ store Arc<dyn Transport> in the entry
has_aardvark()
→ any entry where kind == Aardvark → true / false
resolve_aardvark_device(args)
→ read "device" param (default: "aardvark0")
→ look up alias in HashMap
→ return (alias, DeviceContext{ transport, capabilities })
```
---
### Layer 5 — `boot()` (startup wiring)
**File:** `src/hardware/mod.rs`
`boot()` runs once at startup. For Aardvark:
```
boot()
...
aardvark_ports = aardvark_sys::AardvarkHandle::find_devices()
// → [] in stub mode, [0] if one adapter is plugged in
for (i, port) in aardvark_ports:
alias = registry.register("aardvark", vid=0x2b76, ...)
// → "aardvark0", "aardvark1", ...
transport = AardvarkTransport::new(port, bitrate=100kHz)
registry.attach_transport(alias, transport, {i2c:true, spi:true, gpio:true})
log "[registry] aardvark0 ready → Total Phase port 0"
...
```
---
### Layer 6 — Tool Registry (the loader)
**File:** `src/hardware/tool_registry.rs`
After `boot()`, the tool registry checks what hardware is present and loads
only the relevant tools:
```
ToolRegistry::load(devices)
# always loaded (Pico / GPIO)
register: gpio_write, gpio_read, gpio_toggle, pico_flash, device_list, device_status
# only loaded if an Aardvark was found at boot
if devices.has_aardvark():
register: i2c_scan, i2c_read, i2c_write, spi_transfer, gpio_aardvark, datasheet
```
This is why the `hardware_feature_registers_all_six_tools` test still passes in stub mode — `has_aardvark()` returns false, 0 extra tools load, count stays at 6.
---
## Full Flow Diagram
```
SDK FILES aardvark-sys ZeroClaw core
(vendor/) (crates/) (src/)
─────────────────────────────────────────────────────────────────
aardvark.h ──► build.rs boot()
aardvark.so (bindgen) ──► find_devices()
│ │
bindings.rs │ vec![0] (one adapter)
│ ▼
lib.rs register("aardvark0")
AardvarkHandle attach_transport(AardvarkTransport)
│ │
│ ▼
│ ToolRegistry::load()
│ has_aardvark() == true
│ → load 6 aardvark tools
─────────────────────────────────────────────────────────────────
USER MESSAGE: "scan the i2c bus"
agent loop
I2cScanTool.call()
resolve_aardvark_device("aardvark0")
│ returns transport Arc
AardvarkTransport.send(ZcCommand{ name: "i2c_scan" })
AardvarkHandle::open_port(0) ← opens USB connection
aa_i2c_read(0x08..0x77) ← probes each address
AardvarkHandle dropped ← USB connection closed
ZcResponse{ output: "Found: 0x48, 0x68" }
agent sends reply to user: "I found two I2C devices: 0x48 and 0x68"
```
---
## Stub vs Real Side by Side
| | Stub mode (now) | Real hardware |
|---|---|---|
| `find_devices()` | returns `[]` | returns `[0]` |
| `open_port(0)` | `Err(NotFound)` | opens USB, returns handle |
| `i2c_scan()` | `[]` | probes bus, returns addresses |
| tools loaded | only the 6 Pico tools | 6 Pico + 6 Aardvark tools |
| `has_aardvark()` | `false` | `true` |
| SDK needed | no | yes (`vendor/aardvark.h` + `.so`) |
The only code that changes when you plug in real hardware is inside
`crates/aardvark-sys/src/lib.rs` — every other layer is already wired up
and waiting.

View File

@ -0,0 +1,202 @@
# ADR-004: Tool Shared State Ownership Contract
**Status:** Accepted
**Date:** 2026-03-22
**Issue:** [#4057](https://github.com/zeroclaw/zeroclaw/issues/4057)
## Context
ZeroClaw tools execute in a multi-client environment where a single daemon
process serves requests from multiple connected clients simultaneously. Several
tools already maintain long-lived shared state:
- **`DelegateParentToolsHandle`** (`src/tools/mod.rs`):
`Arc<RwLock<Vec<Arc<dyn Tool>>>>` — holds parent tools for delegate agents
with no per-client isolation.
- **`ChannelMapHandle`** (`src/tools/reaction.rs`):
`Arc<RwLock<HashMap<String, Arc<dyn Channel>>>>` — global channel map shared
across all clients.
- **`CanvasStore`** (`src/tools/canvas.rs`):
`Arc<RwLock<HashMap<String, CanvasEntry>>>` — canvas IDs are plain strings
with no client namespace.
These patterns emerged organically. As the tool surface grows and more clients
connect concurrently, we need a clear contract governing ownership, identity,
isolation, lifecycle, and reload behavior for tool-held shared state. Without
this contract, new tools risk introducing data leaks between clients, stale
state after config reloads, or inconsistent initialization timing.
Additional context:
- The tool registry is immutable after startup, built once in
`all_tools_with_runtime()`.
- Client identity is currently derived from IP address only
(`src/gateway/mod.rs`), which is insufficient for reliable namespacing.
- `SecurityPolicy` is scoped per agent, not per client.
- `WorkspaceManager` provides some isolation but workspace switching is global.
## Decision
### 1. Ownership: May tools own long-lived shared state?
**Yes.** Tools MAY own long-lived shared state, provided they follow the
established **handle pattern**: wrap the state in `Arc<RwLock<T>>` (or
`Arc<parking_lot::RwLock<T>>`) and expose a cloneable handle type.
This pattern is already proven by three independent implementations:
| Handle | Location | Inner type |
|--------|----------|-----------|
| `DelegateParentToolsHandle` | `src/tools/mod.rs` | `Vec<Arc<dyn Tool>>` |
| `ChannelMapHandle` | `src/tools/reaction.rs` | `HashMap<String, Arc<dyn Channel>>` |
| `CanvasStore` | `src/tools/canvas.rs` | `HashMap<String, CanvasEntry>` |
Tools that need shared state MUST:
- Define a named handle type alias (e.g., `pub type FooHandle = Arc<RwLock<T>>`).
- Accept the handle at construction time rather than creating global state.
- Document the concurrency contract in the handle type's doc comment.
Tools MUST NOT use static mutable state (`lazy_static!`, `OnceCell` with
interior mutability) for per-request or per-client data.
### 2. Identity assignment: Who constructs identity keys?
**The daemon SHOULD provide identity.** Tools MUST NOT construct their own
client identity keys.
A new `ClientId` type should be introduced (opaque, `Clone + Eq + Hash + Send + Sync`)
that the daemon assigns at connection time. This replaces the current approach
of using raw IP addresses (`src/gateway/mod.rs:259-306`), which breaks when
multiple clients share a NAT address or when proxied connections arrive.
`ClientId` is passed to tools that require per-client state namespacing as part
of the tool execution context. Tools that do not need per-client isolation
(e.g., the immutable tool registry) may ignore it.
The `ClientId` contract:
- Generated by the gateway layer at connection establishment.
- Opaque to tools — tools must not parse or derive meaning from the value.
- Stable for the lifetime of a single client session.
- Passed through the execution context, not stored globally.
### 3. Lifecycle: When may tools run startup-style validation?
**Validation runs once at first registration, and again when config changes
are detected.**
The lifecycle phases are:
1. **Construction** — tool is instantiated with handles and config. No I/O or
validation occurs here.
2. **Registration** — tool is registered in the tool registry via
`all_tools_with_runtime()`. At this point the tool MAY perform one-time
startup validation (e.g., checking that required credentials exist, verifying
external service connectivity).
3. **Execution** — tool handles individual requests. No re-validation unless
the config-change signal fires (see Reload Semantics below).
4. **Shutdown** — daemon is stopping. Tools with open resources SHOULD clean up
gracefully via `Drop` or an explicit shutdown method.
Tools MUST NOT perform blocking validation during execution-phase calls.
Validation results SHOULD be cached in the tool's handle state and checked
via a fast path during execution.
### 4. Isolation: What must be isolated per client?
State falls into two categories with different isolation requirements:
**MUST be isolated per client:**
- Security-sensitive state: credentials, API keys, quotas, rate-limit counters,
per-client authorization decisions.
- User-specific session data: conversation context, user preferences,
workspace-scoped file paths.
Isolation mechanism: tools holding per-client state MUST key their internal
maps by `ClientId`. The handle pattern naturally supports this by using
`HashMap<ClientId, T>` inside the `RwLock`.
**MAY be shared across clients (with namespace prefixing):**
- Broadcast/display state: canvas frames (`CanvasStore`), notification channels
(`ChannelMapHandle`).
- Read-only reference data: tool registry, static configuration, model
metadata.
When shared state uses string keys (e.g., canvas IDs, channel names), tools
SHOULD support optional namespace prefixing (e.g., `{client_id}:{canvas_name}`)
to allow per-client isolation when needed without mandating it for broadcast
use cases.
Tools MUST NOT store per-client secrets in shared (non-isolated) state
structures.
### 5. Reload semantics: What invalidates prior shared state on config change?
**Config changes detected via hash comparison MUST invalidate cached
validation state.**
The reload contract:
- The daemon computes a hash of the tool-relevant config section at startup and
after each config reload event.
- When the hash changes, the daemon signals affected tools to re-run their
registration-phase validation.
- Tools MUST treat their cached validation result as stale when signaled and
re-validate before the next execution.
Specific invalidation rules:
| Config change | Invalidation scope |
|--------------|-------------------|
| Credential/secret rotation | Per-tool validation cache; per-client credential state |
| Tool enable/disable | Full tool registry rebuild via `all_tools_with_runtime()` |
| Security policy change | `SecurityPolicy` re-derivation; per-agent policy state |
| Workspace directory change | `WorkspaceManager` state; file-path-dependent tool state |
| Provider config change | Provider-dependent tools re-validate connectivity |
Tools MAY retain non-security shared state (e.g., canvas content, channel
subscriptions) across config reloads unless the reload explicitly affects that
state's validity.
## Consequences
### Positive
- **Consistency:** All new tools follow the same handle pattern, making shared
state discoverable and auditable.
- **Safety:** Per-client isolation of security-sensitive state prevents data
leaks in multi-tenant scenarios.
- **Clarity:** Explicit lifecycle phases eliminate ambiguity about when
validation runs.
- **Evolvability:** The `ClientId` abstraction decouples tools from transport
details, supporting future identity mechanisms (tokens, certificates).
### Negative
- **Migration cost:** Existing tools (`CanvasStore`, `ReactionTool`) may need
refactoring to accept `ClientId` and namespace their state.
- **Complexity:** Tools that were simple singletons now need to consider
multi-client semantics even if they currently have one client.
- **Performance:** Per-client keying adds a hash lookup on each access, though
this is negligible compared to I/O costs.
### Neutral
- The tool registry remains immutable after startup; this ADR does not change
that invariant.
- `SecurityPolicy` remains per-agent; this ADR documents that client isolation
is orthogonal to agent-level policy.
## References
- `src/tools/mod.rs``DelegateParentToolsHandle`, `all_tools_with_runtime()`
- `src/tools/reaction.rs``ChannelMapHandle`, `ReactionTool`
- `src/tools/canvas.rs``CanvasStore`, `CanvasEntry`
- `src/tools/traits.rs``Tool` trait
- `src/gateway/mod.rs` — client IP extraction (`forwarded_client_ip`, `resolve_client_ip`)
- `src/security/``SecurityPolicy`

215
docs/browser-setup.md Normal file
View File

@ -0,0 +1,215 @@
# Browser Automation Setup Guide
This guide covers setting up browser automation capabilities in ZeroClaw, including both headless automation and GUI access via VNC.
## Overview
ZeroClaw supports multiple browser access methods:
| Method | Use Case | Requirements |
|--------|----------|--------------|
| **agent-browser CLI** | Headless automation, AI agents | npm, Chrome |
| **VNC + noVNC** | GUI access, debugging | Xvfb, x11vnc, noVNC |
| **Chrome Remote Desktop** | Remote GUI via Google | XFCE, Google account |
## Quick Start: Headless Automation
### 1. Install agent-browser
```bash
# Install CLI
npm install -g agent-browser
# Download Chrome for Testing
agent-browser install --with-deps # Linux (includes system deps)
agent-browser install # macOS/Windows
```
### 2. Verify ZeroClaw Config
The browser tool is enabled by default. To verify or customize, edit
`~/.zeroclaw/config.toml`:
```toml
[browser]
enabled = true # default: true
allowed_domains = ["*"] # default: ["*"] (all public hosts)
backend = "agent_browser" # default: "agent_browser"
native_headless = true # default: true
```
To restrict domains or disable the browser tool:
```toml
[browser]
enabled = false # disable entirely
# or restrict to specific domains:
allowed_domains = ["example.com", "docs.example.com"]
```
### 3. Test
```bash
echo "Open https://example.com and tell me what it says" | zeroclaw agent
```
## VNC Setup (GUI Access)
For debugging or when you need visual browser access:
### Install Dependencies
```bash
# Ubuntu/Debian
apt-get install -y xvfb x11vnc fluxbox novnc websockify
# Optional: Desktop environment for Chrome Remote Desktop
apt-get install -y xfce4 xfce4-goodies
```
### Start VNC Server
```bash
#!/bin/bash
# Start virtual display with VNC access
DISPLAY_NUM=99
VNC_PORT=5900
NOVNC_PORT=6080
RESOLUTION=1920x1080x24
# Start Xvfb
Xvfb :$DISPLAY_NUM -screen 0 $RESOLUTION -ac &
sleep 1
# Start window manager
fluxbox -display :$DISPLAY_NUM &
sleep 1
# Start x11vnc
x11vnc -display :$DISPLAY_NUM -rfbport $VNC_PORT -forever -shared -nopw -bg
sleep 1
# Start noVNC (web-based VNC)
websockify --web=/usr/share/novnc $NOVNC_PORT localhost:$VNC_PORT &
echo "VNC available at:"
echo " VNC Client: localhost:$VNC_PORT"
echo " Web Browser: http://localhost:$NOVNC_PORT/vnc.html"
```
### VNC Access
- **VNC Client**: Connect to `localhost:5900`
- **Web Browser**: Open `http://localhost:6080/vnc.html`
### Start Browser on VNC Display
```bash
DISPLAY=:99 google-chrome --no-sandbox https://example.com &
```
## Chrome Remote Desktop
### Install
```bash
# Download and install
wget https://dl.google.com/linux/direct/chrome-remote-desktop_current_amd64.deb
apt-get install -y ./chrome-remote-desktop_current_amd64.deb
# Configure session
echo "xfce4-session" > ~/.chrome-remote-desktop-session
chmod +x ~/.chrome-remote-desktop-session
```
### Setup
1. Visit <https://remotedesktop.google.com/headless>
2. Copy the "Debian Linux" setup command
3. Run it on your server
4. Start the service: `systemctl --user start chrome-remote-desktop`
### Remote Access
Go to <https://remotedesktop.google.com/access> from any device.
## Testing
### CLI Tests
```bash
# Basic open and close
agent-browser open https://example.com
agent-browser get title
agent-browser close
# Snapshot with refs
agent-browser open https://example.com
agent-browser snapshot -i
agent-browser close
# Screenshot
agent-browser open https://example.com
agent-browser screenshot /tmp/test.png
agent-browser close
```
### ZeroClaw Integration Tests
```bash
# Content extraction
echo "Open https://example.com and summarize it" | zeroclaw agent
# Navigation
echo "Go to https://github.com/trending and list the top 3 repos" | zeroclaw agent
# Form interaction
echo "Go to Wikipedia, search for 'Rust programming language', and summarize" | zeroclaw agent
```
## Troubleshooting
### "Element not found"
The page may not be fully loaded. Add a wait:
```bash
agent-browser open https://slow-site.com
agent-browser wait --load networkidle
agent-browser snapshot -i
```
### Cookie dialogs blocking access
Handle cookie consent first:
```bash
agent-browser open https://site-with-cookies.com
agent-browser snapshot -i
agent-browser click @accept_cookies # Click the accept button
agent-browser snapshot -i # Now get the actual content
```
### Docker sandbox network restrictions
If `web_fetch` fails inside Docker sandbox, use agent-browser instead:
```bash
# Instead of web_fetch, use:
agent-browser open https://example.com
agent-browser get text body
```
## Security Notes
- `agent-browser` runs Chrome in headless mode with sandboxing
- For sensitive sites, use `--session-name` to persist auth state
- The `--allowed-domains` config restricts navigation to specific domains
- VNC ports (5900, 6080) should be behind a firewall or Tailscale
## Related
- [agent-browser Documentation](https://github.com/vercel-labs/agent-browser)
- [ZeroClaw Configuration Reference](./config-reference.md)
- [Skills Documentation](../skills/)

View File

@ -20,6 +20,7 @@ Selected allowlist (all actions currently used across Quality Gate, Release Beta
| `docker/setup-buildx-action@v3` | release, promote-release | Docker Buildx setup |
| `docker/login-action@v3` | release, promote-release | GHCR authentication |
| `docker/build-push-action@v6` | release, promote-release | Multi-platform Docker image build and push |
| `actions/labeler@v5` | pr-path-labeler | Apply path/scope labels from `labeler.yml` |
Equivalent allowlist patterns:
@ -36,6 +37,7 @@ Equivalent allowlist patterns:
| Quality Gate | `.github/workflows/checks-on-pr.yml` | Pull requests to `master` |
| Release Beta | `.github/workflows/release-beta-on-push.yml` | Push to `master` |
| Release Stable | `.github/workflows/release-stable-manual.yml` | Manual `workflow_dispatch` |
| PR Path Labeler | `.github/workflows/pr-path-labeler.yml` | `pull_request_target` (opened, synchronize, reopened) |
## Change Control
@ -62,6 +64,7 @@ gh api repos/zeroclaw-labs/zeroclaw/actions/permissions/selected-actions
## Change Log
- 2026-03-23: Added PR Path Labeler (`pr-path-labeler.yml`) using `actions/labeler@v5`. No allowlist change needed — covered by existing `actions/*` pattern.
- 2026-03-10: Renamed workflows — CI → Quality Gate (`checks-on-pr.yml`), Beta Release → Release Beta (`release-beta-on-push.yml`), Promote Release → Release Stable (`release-stable-manual.yml`). Added `lint` and `security` jobs to Quality Gate. Added Cross-Platform Build (`cross-platform-build-manual.yml`).
- 2026-03-05: Complete workflow overhaul — replaced 22 workflows with 3 (CI, Beta Release, Promote Release)
- Removed patterns no longer in use: `DavidAnson/markdownlint-cli2-action@*`, `lycheeverse/lychee-action@*`, `EmbarkStudios/cargo-deny-action@*`, `rustsec/audit-check@*`, `rhysd/actionlint@*`, `sigstore/cosign-installer@*`, `Checkmarx/vorpal-reviewdog-github-action@*`, `useblacksmith/*`

View File

@ -45,6 +45,15 @@ For complete code examples of each extension trait, see [extension-examples.md](
- Keep multilingual entry-point parity for all supported locales (`en`, `zh-CN`, `ja`, `ru`, `fr`, `vi`) when nav or key wording changes.
- When shared docs wording changes, sync corresponding localized docs in the same PR (or explicitly document deferral and follow-up PR).
## Tool Shared State
- Follow the `Arc<RwLock<T>>` handle pattern for any tool that owns long-lived shared state.
- Accept handles at construction; do not create global/static mutable state.
- Use `ClientId` (provided by the daemon) to namespace per-client state — never construct identity keys inside the tool.
- Isolate security-sensitive state (credentials, quotas) per client; broadcast/display state may be shared with optional namespace prefixing.
- Cached validation is invalidated on config change — tools must re-validate before the next execution when signaled.
- See [ADR-004: Tool Shared State Ownership](../architecture/adr-004-tool-shared-state-ownership.md) for the full contract.
## Architecture Boundary Rules
- Extend capabilities by adding trait implementations + factory wiring first; avoid cross-module rewrites for isolated features.

View File

@ -0,0 +1,213 @@
# Label Registry
Single reference for every label used on PRs and issues. Labels are grouped by category. Each entry lists the label name, definition, and how it is applied.
Sources consolidated here:
- `.github/labeler.yml` (path-label config for `actions/labeler`)
- `.github/label-policy.json` (contributor tier thresholds)
- `docs/contributing/pr-workflow.md` (size, risk, and triage label definitions)
- `docs/contributing/ci-map.md` (automation behavior and high-risk path heuristics)
Note: The CI was simplified to 4 workflows (`ci.yml`, `release.yml`, `ci-full.yml`, `promote-release.yml`). Workflows that previously automated size, risk, contributor tier, and triage labels (`pr-labeler.yml`, `pr-auto-response.yml`, `pr-check-stale.yml`, and supporting scripts) were removed. Only path labels via `pr-path-labeler.yml` are currently automated.
---
## Path labels
Applied automatically by `pr-path-labeler.yml` using `actions/labeler`. Matches changed files against glob patterns in `.github/labeler.yml`.
### Base scope labels
| Label | Matches |
|---|---|
| `docs` | `docs/**`, `**/*.md`, `**/*.mdx`, `LICENSE`, `.markdownlint-cli2.yaml` |
| `dependencies` | `Cargo.toml`, `Cargo.lock`, `deny.toml`, `.github/dependabot.yml` |
| `ci` | `.github/**`, `.githooks/**` |
| `core` | `src/*.rs` |
| `agent` | `src/agent/**` |
| `channel` | `src/channels/**` |
| `gateway` | `src/gateway/**` |
| `config` | `src/config/**` |
| `cron` | `src/cron/**` |
| `daemon` | `src/daemon/**` |
| `doctor` | `src/doctor/**` |
| `health` | `src/health/**` |
| `heartbeat` | `src/heartbeat/**` |
| `integration` | `src/integrations/**` |
| `memory` | `src/memory/**` |
| `security` | `src/security/**` |
| `runtime` | `src/runtime/**` |
| `onboard` | `src/onboard/**` |
| `provider` | `src/providers/**` |
| `service` | `src/service/**` |
| `skillforge` | `src/skillforge/**` |
| `skills` | `src/skills/**` |
| `tool` | `src/tools/**` |
| `tunnel` | `src/tunnel/**` |
| `observability` | `src/observability/**` |
| `tests` | `tests/**` |
| `scripts` | `scripts/**` |
| `dev` | `dev/**` |
### Per-component channel labels
Each channel gets a specific label in addition to the base `channel` label.
| Label | Matches |
|---|---|
| `channel:bluesky` | `bluesky.rs` |
| `channel:clawdtalk` | `clawdtalk.rs` |
| `channel:cli` | `cli.rs` |
| `channel:dingtalk` | `dingtalk.rs` |
| `channel:discord` | `discord.rs`, `discord_history.rs` |
| `channel:email` | `email_channel.rs`, `gmail_push.rs` |
| `channel:imessage` | `imessage.rs` |
| `channel:irc` | `irc.rs` |
| `channel:lark` | `lark.rs` |
| `channel:linq` | `linq.rs` |
| `channel:matrix` | `matrix.rs` |
| `channel:mattermost` | `mattermost.rs` |
| `channel:mochat` | `mochat.rs` |
| `channel:mqtt` | `mqtt.rs` |
| `channel:nextcloud-talk` | `nextcloud_talk.rs` |
| `channel:nostr` | `nostr.rs` |
| `channel:notion` | `notion.rs` |
| `channel:qq` | `qq.rs` |
| `channel:reddit` | `reddit.rs` |
| `channel:signal` | `signal.rs` |
| `channel:slack` | `slack.rs` |
| `channel:telegram` | `telegram.rs` |
| `channel:twitter` | `twitter.rs` |
| `channel:wati` | `wati.rs` |
| `channel:webhook` | `webhook.rs` |
| `channel:wecom` | `wecom.rs` |
| `channel:whatsapp` | `whatsapp.rs`, `whatsapp_storage.rs`, `whatsapp_web.rs` |
### Per-component provider labels
| Label | Matches |
|---|---|
| `provider:anthropic` | `anthropic.rs` |
| `provider:azure-openai` | `azure_openai.rs` |
| `provider:bedrock` | `bedrock.rs` |
| `provider:claude-code` | `claude_code.rs` |
| `provider:compatible` | `compatible.rs` |
| `provider:copilot` | `copilot.rs` |
| `provider:gemini` | `gemini.rs`, `gemini_cli.rs` |
| `provider:glm` | `glm.rs` |
| `provider:kilocli` | `kilocli.rs` |
| `provider:ollama` | `ollama.rs` |
| `provider:openai` | `openai.rs`, `openai_codex.rs` |
| `provider:openrouter` | `openrouter.rs` |
| `provider:telnyx` | `telnyx.rs` |
### Per-group tool labels
Tools are grouped by logical function rather than one label per file.
| Label | Matches |
|---|---|
| `tool:browser` | `browser.rs`, `browser_delegate.rs`, `browser_open.rs`, `text_browser.rs`, `screenshot.rs` |
| `tool:cloud` | `cloud_ops.rs`, `cloud_patterns.rs` |
| `tool:composio` | `composio.rs` |
| `tool:cron` | `cron_add.rs`, `cron_list.rs`, `cron_remove.rs`, `cron_run.rs`, `cron_runs.rs`, `cron_update.rs` |
| `tool:file` | `file_edit.rs`, `file_read.rs`, `file_write.rs`, `glob_search.rs`, `content_search.rs` |
| `tool:google-workspace` | `google_workspace.rs` |
| `tool:mcp` | `mcp_client.rs`, `mcp_deferred.rs`, `mcp_protocol.rs`, `mcp_tool.rs`, `mcp_transport.rs` |
| `tool:memory` | `memory_forget.rs`, `memory_recall.rs`, `memory_store.rs` |
| `tool:microsoft365` | `microsoft365/**` |
| `tool:security` | `security_ops.rs`, `verifiable_intent.rs` |
| `tool:shell` | `shell.rs`, `node_tool.rs`, `cli_discovery.rs` |
| `tool:sop` | `sop_advance.rs`, `sop_approve.rs`, `sop_execute.rs`, `sop_list.rs`, `sop_status.rs` |
| `tool:web` | `web_fetch.rs`, `web_search_tool.rs`, `web_search_provider_routing.rs`, `http_request.rs` |
---
## Size labels
Defined in `pr-workflow.md` §6.1. Based on effective changed line count, normalized for docs-only and lockfile-heavy PRs.
| Label | Threshold |
|---|---|
| `size: XS` | <= 80 lines |
| `size: S` | <= 250 lines |
| `size: M` | <= 500 lines |
| `size: L` | <= 1000 lines |
| `size: XL` | > 1000 lines |
**Applied by:** manual. The workflows that previously computed size labels (`pr-labeler.yml` and supporting scripts) were removed during CI simplification.
---
## Risk labels
Defined in `pr-workflow.md` §13.2 and `ci-map.md`. Based on a heuristic combining touched paths and change size.
| Label | Meaning |
|---|---|
| `risk: low` | No high-risk paths touched, small change |
| `risk: medium` | Behavioral `src/**` changes without boundary/security impact |
| `risk: high` | Touches high-risk paths (see below) or large security-adjacent change |
| `risk: manual` | Maintainer override that freezes automated risk recalculation |
High-risk paths: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**`.
The boundary between low and medium is not formally defined beyond "no high-risk paths."
**Applied by:** manual. Previously automated via `pr-labeler.yml`; removed during CI simplification.
---
## Contributor tier labels
Defined in `.github/label-policy.json`. Based on the author's merged PR count queried from the GitHub API.
| Label | Minimum merged PRs |
|---|---|
| `trusted contributor` | 5 |
| `experienced contributor` | 10 |
| `principal contributor` | 20 |
| `distinguished contributor` | 50 |
**Applied by:** manual. Previously automated via `pr-labeler.yml` and `pr-auto-response.yml`; removed during CI simplification.
---
## Response and triage labels
Defined in `pr-workflow.md` §8. Applied manually.
| Label | Purpose | Applied by |
|---|---|---|
| `r:needs-repro` | Incomplete bug report; request deterministic repro | Manual |
| `r:support` | Usage/help item better handled outside bug backlog | Manual |
| `invalid` | Not a valid bug/feature request | Manual |
| `duplicate` | Duplicate of existing issue | Manual |
| `stale-candidate` | Dormant PR/issue; candidate for closing | Manual |
| `superseded` | Replaced by a newer PR | Manual |
| `no-stale` | Exempt from stale automation; accepted but blocked work | Manual |
**Automation:** none currently. The workflows that handled label-driven issue closing (`pr-auto-response.yml`) and stale detection (`pr-check-stale.yml`) were removed during CI simplification.
---
## Implementation status
| Category | Count | Automated | Workflow |
|---|---|---|---|
| Path (base scope) | 27 | Yes | `pr-path-labeler.yml` |
| Path (per-component) | 52 | Yes | `pr-path-labeler.yml` |
| Size | 5 | No | Manual |
| Risk | 4 | No | Manual |
| Contributor tier | 4 | No | Manual |
| Response/triage | 7 | No | Manual |
| **Total** | **99** | | |
---
## Maintenance
- **Owner:** maintainers responsible for label policy and PR triage automation.
- **Update trigger:** new channels, providers, or tools added to the source tree; label policy changes; triage workflow changes.
- **Source of truth:** this document consolidates definitions from the four source files listed at the top. When definitions conflict, update the source file first, then sync this registry.

Some files were not shown because too many files have changed in this diff Show More