Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 314e1d3ae8 | |||
| 82be05b1e9 | |||
| 1373659058 | |||
| c7f064e866 | |||
| 9c1d63e109 | |||
| 966edf1553 | |||
| a1af84d992 | |||
| 0ad1965081 | |||
| 70e8e7ebcd | |||
| 2bcb82c5b3 | |||
| e211b5c3e3 | |||
| 8691476577 | |||
| e34a804255 | |||
| 6120b3f705 | |||
| f175261e32 | |||
| fd9f66cad7 | |||
| d928ebc92e | |||
| 9fca9f478a | |||
| 7106632b51 | |||
| b834278754 | |||
| 186f6d9797 | |||
| 6cdc92a256 | |||
| 02599dcd3c | |||
| fe64d7ef7e | |||
| 996dbe95cf | |||
| 45f953be6d | |||
| 82f29bbcb1 | |||
| 93b5a0b824 | |||
| 08a67c4a2d | |||
| c86a0673ba | |||
| cabf99ba07 | |||
| 2d978a6b64 | |||
| 4dbc9266c1 | |||
| ea0b3c8c8c | |||
| 0c56834385 | |||
| caccf0035e | |||
| 627b160f55 | |||
| 6463bc84b0 | |||
| f84f1229af | |||
| f85d21097b | |||
| 306821d6a2 | |||
| 06f9424274 | |||
| fa14ab4ab2 | |||
| 36a0c8eba9 | |||
| f4c82d5797 | |||
| 5edebf4869 | |||
| 613fa79444 | |||
| 44c8e1eaac | |||
| 414b1fa8dd | |||
| 913d5ee851 | |||
| 5a4332d0e4 | |||
| 970c80d278 | |||
| 9514c20038 | |||
| 9cf31a732c | |||
| 7f0ddf06a9 | |||
| b79e88662e | |||
| b71b69fa6e | |||
| 8b961b7bd9 | |||
| 3b67d8a797 | |||
| f0e111bf7b | |||
| b3071f622a | |||
| 8dbf142c7b | |||
| 2b3603bacf | |||
| 02369d892d | |||
| b38db505ff | |||
| 06b9525263 | |||
| 8fe573330c | |||
| dc61f08f60 | |||
| f7c8a3b8be | |||
| 6cd8749eb4 | |||
| c54f374c7e | |||
| dd00b57f6f | |||
| c8db32389d | |||
| 611e4702c2 | |||
| 99cfae1f00 | |||
| 1c624f0c51 | |||
| 8a8f946269 | |||
| 17469945f7 | |||
| 6dcd7b7222 | |||
| 342f743720 | |||
| e05fd75763 | |||
| 4daeb380a0 | |||
| ddacb2a917 | |||
| 77ca576be6 | |||
| 5a4f69e71f | |||
| e952838eef | |||
| 3fc4e66fb6 | |||
| 0a331a1440 | |||
| 5919becab9 | |||
| 287d9bdc17 | |||
| f900d7079e | |||
| 5a5b5a4402 | |||
| 06f65fb711 | |||
| 46d4b13c22 | |||
| 8fcbb6eb2d | |||
| ce22eba7d0 | |||
| 7ba4d06e78 | |||
| dc12d03876 | |||
| 3151604b04 | |||
| c5fcda06ad | |||
| 51a52dcadb | |||
| dd9e26eac6 | |||
| c4b2a21c61 | |||
| d6170ab49b | |||
| 399c896c3b | |||
| 71d32c3b04 | |||
| 27936b051d | |||
| 39d788a95f | |||
| 5d921bd37d | |||
| d17f0a946c | |||
| 2710ce65cc | |||
| 51e8fc8423 | |||
| ecf9d477bd | |||
| 625784c25f |
@@ -53,7 +53,7 @@ If the user hasn't set up ZeroClaw yet (no `~/.zeroclaw/config.toml` exists), gu
|
||||
```bash
|
||||
zeroclaw onboard # Quick mode — defaults to OpenRouter
|
||||
zeroclaw onboard --provider anthropic # Use Anthropic directly
|
||||
zeroclaw onboard --interactive # Step-by-step wizard
|
||||
zeroclaw onboard # Guided wizard (default)
|
||||
```
|
||||
|
||||
After onboarding, verify everything works:
|
||||
|
||||
@@ -50,7 +50,7 @@ First-time setup or reconfiguration.
|
||||
```bash
|
||||
zeroclaw onboard # Quick mode (default: openrouter)
|
||||
zeroclaw onboard --provider anthropic # Quick mode with specific provider
|
||||
zeroclaw onboard --interactive # Interactive wizard
|
||||
zeroclaw onboard # Guided wizard (default)
|
||||
zeroclaw onboard --memory sqlite # Set memory backend
|
||||
zeroclaw onboard --force # Overwrite existing config
|
||||
zeroclaw onboard --channels-only # Repair channels only
|
||||
@@ -62,7 +62,7 @@ zeroclaw onboard --channels-only # Repair channels only
|
||||
- `--memory <backend>` — sqlite, markdown, lucid, none
|
||||
- `--force` — overwrite existing config.toml
|
||||
- `--channels-only` — only repair channel configuration
|
||||
- `--interactive` — step-by-step wizard
|
||||
- `--reinit` — start fresh (backs up existing config)
|
||||
|
||||
Creates `~/.zeroclaw/config.toml` with `0600` permissions.
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ body:
|
||||
label: Steps to reproduce
|
||||
description: Please provide exact commands/config.
|
||||
placeholder: |
|
||||
1. zeroclaw onboard --interactive
|
||||
1. zeroclaw onboard
|
||||
2. zeroclaw daemon
|
||||
3. Observe crash in logs
|
||||
render: bash
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -77,6 +77,8 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
@@ -84,6 +86,7 @@ jobs:
|
||||
toolchain: 1.92.0
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
if: runner.os != 'Windows'
|
||||
|
||||
- name: Install mold linker
|
||||
if: runner.os == 'Linux'
|
||||
@@ -92,6 +95,7 @@ jobs:
|
||||
sudo apt-get install -y mold
|
||||
|
||||
- name: Ensure web/dist placeholder exists
|
||||
shell: bash
|
||||
run: mkdir -p web/dist && touch web/dist/.gitkeep
|
||||
|
||||
- name: Build release
|
||||
|
||||
@@ -105,6 +105,8 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
@@ -112,6 +114,7 @@ jobs:
|
||||
toolchain: 1.92.0
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
if: runner.os != 'Windows'
|
||||
|
||||
- name: Install mold linker
|
||||
if: runner.os == 'Linux'
|
||||
@@ -120,6 +123,7 @@ jobs:
|
||||
sudo apt-get install -y mold
|
||||
|
||||
- name: Ensure web/dist placeholder exists
|
||||
shell: bash
|
||||
run: mkdir -p web/dist && touch web/dist/.gitkeep
|
||||
|
||||
- name: Build release
|
||||
|
||||
@@ -81,7 +81,7 @@ Current maintainers with PR approval authority: `theonlyhennygod`, `JordanTheJet
|
||||
| `aarch64-unknown-linux-gnu` | | ✓ | ✓ | ✓ |
|
||||
| `aarch64-apple-darwin` | ✓ | | ✓ | ✓ |
|
||||
| `x86_64-apple-darwin` | | ✓ | | |
|
||||
| `x86_64-pc-windows-msvc` | | ✓ | ✓ | ✓ |
|
||||
| `x86_64-pc-windows-msvc` | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
## Mermaid Diagrams
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
name: Auto-sync crates.io
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "Cargo.toml"
|
||||
|
||||
concurrency:
|
||||
group: publish-crates-auto
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
detect-version-change:
|
||||
name: Detect Version Bump
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.check.outputs.changed }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
current=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
|
||||
previous=$(git show HEAD~1:Cargo.toml 2>/dev/null | sed -n 's/^version = "\([^"]*\)"/\1/p' | head -1 || echo "")
|
||||
|
||||
echo "Current version: ${current}"
|
||||
echo "Previous version: ${previous}"
|
||||
|
||||
if [[ "$current" != "$previous" && -n "$current" ]]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${current}" >> "$GITHUB_OUTPUT"
|
||||
echo "Version bumped from ${previous} to ${current} — will publish"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Version unchanged (${current}) — skipping publish"
|
||||
fi
|
||||
|
||||
check-registry:
|
||||
name: Check if Already Published
|
||||
needs: [detect-version-change]
|
||||
if: needs.detect-version-change.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_publish: ${{ steps.check.outputs.should_publish }}
|
||||
steps:
|
||||
- name: Check crates.io for existing version
|
||||
id: check
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.detect-version-change.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
"https://crates.io/api/v1/crates/zeroclawlabs/${VERSION}")
|
||||
|
||||
if [[ "$status" == "200" ]]; then
|
||||
echo "Version ${VERSION} already exists on crates.io — skipping"
|
||||
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Version ${VERSION} not yet published — proceeding"
|
||||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
needs: [detect-version-change, check-registry]
|
||||
if: needs.check-registry.outputs.should_publish == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.92.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
|
||||
- name: Build web dashboard
|
||||
run: cd web && npm ci && npm run build
|
||||
|
||||
- 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 to crates.io
|
||||
run: cargo publish --locked --allow-dirty --no-verify
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Verify published
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.detect-version-change.outputs.version }}
|
||||
run: |
|
||||
echo "Waiting for crates.io to index..."
|
||||
sleep 15
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
"https://crates.io/api/v1/crates/zeroclawlabs/${VERSION}")
|
||||
if [[ "$status" == "200" ]]; then
|
||||
echo "zeroclawlabs v${VERSION} is live on crates.io"
|
||||
echo "Install: cargo install zeroclawlabs"
|
||||
else
|
||||
echo "::warning::Version may still be indexing — check https://crates.io/crates/zeroclawlabs"
|
||||
fi
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Publish to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 0.2.0) — must match Cargo.toml"
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Dry run (validate without publishing)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: publish-crates
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check version matches Cargo.toml
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
|
||||
if [[ "$cargo_version" != "$INPUT_VERSION" ]]; then
|
||||
echo "::error::Cargo.toml version (${cargo_version}) does not match input (${INPUT_VERSION})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
needs: [validate]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.92.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
|
||||
- name: Build web dashboard
|
||||
run: cd web && npm ci && npm run build
|
||||
|
||||
- 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 (dry run)
|
||||
if: inputs.dry_run
|
||||
run: cargo publish --dry-run --locked --allow-dirty --no-verify
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Publish to crates.io
|
||||
if: "!inputs.dry_run"
|
||||
run: cargo publish --locked --allow-dirty --no-verify
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
@@ -37,6 +37,96 @@ jobs:
|
||||
echo "tag=${beta_tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "Beta release: ${beta_tag}"
|
||||
|
||||
release-notes:
|
||||
name: Generate Release Notes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
notes: ${{ steps.notes.outputs.body }}
|
||||
features: ${{ steps.notes.outputs.features }}
|
||||
contributors: ${{ steps.notes.outputs.contributors }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build release notes
|
||||
id: notes
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Use a wider range — find the previous stable tag to capture all
|
||||
# contributors across the full release cycle, not just one beta bump
|
||||
PREV_TAG=$(git tag --sort=-creatordate \
|
||||
| grep -vE '\-beta\.' \
|
||||
| head -1 || echo "")
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
RANGE="HEAD"
|
||||
else
|
||||
RANGE="${PREV_TAG}..HEAD"
|
||||
fi
|
||||
|
||||
# Extract features only (feat commits) — skip bug fixes for clean notes
|
||||
FEATURES=$(git log "$RANGE" --pretty=format:"%s" --no-merges \
|
||||
| grep -iE '^feat(\(|:)' \
|
||||
| sed 's/^feat(\([^)]*\)): /\1: /' \
|
||||
| sed 's/^feat: //' \
|
||||
| sed 's/ (#[0-9]*)$//' \
|
||||
| sort -uf \
|
||||
| while IFS= read -r line; do echo "- ${line}"; done || true)
|
||||
|
||||
if [ -z "$FEATURES" ]; then
|
||||
FEATURES="- Incremental improvements and polish"
|
||||
fi
|
||||
|
||||
# Collect ALL unique contributors: git authors + Co-Authored-By
|
||||
GIT_AUTHORS=$(git log "$RANGE" --pretty=format:"%an" --no-merges | sort -uf || true)
|
||||
CO_AUTHORS=$(git log "$RANGE" --pretty=format:"%b" --no-merges \
|
||||
| grep -ioE 'Co-Authored-By: *[^<]+' \
|
||||
| sed 's/Co-Authored-By: *//i' \
|
||||
| sed 's/ *$//' \
|
||||
| sort -uf || true)
|
||||
|
||||
# Merge, deduplicate, and filter out bots
|
||||
ALL_CONTRIBUTORS=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \
|
||||
| sort -uf \
|
||||
| grep -v '^$' \
|
||||
| grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \
|
||||
| while IFS= read -r name; do echo "- ${name}"; done || true)
|
||||
|
||||
# Build release body
|
||||
BODY=$(cat <<NOTES_EOF
|
||||
## What's New
|
||||
|
||||
${FEATURES}
|
||||
|
||||
## Contributors
|
||||
|
||||
${ALL_CONTRIBUTORS}
|
||||
|
||||
---
|
||||
*Full changelog: ${PREV_TAG}...HEAD*
|
||||
NOTES_EOF
|
||||
)
|
||||
|
||||
# Output multiline values
|
||||
{
|
||||
echo "body<<BODY_EOF"
|
||||
echo "$BODY"
|
||||
echo "BODY_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo "features<<FEAT_EOF"
|
||||
echo "$FEATURES"
|
||||
echo "FEAT_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo "contributors<<CONTRIB_EOF"
|
||||
echo "$ALL_CONTRIBUTORS"
|
||||
echo "CONTRIB_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
web:
|
||||
name: Build Web Dashboard
|
||||
runs-on: ubuntu-latest
|
||||
@@ -80,6 +170,11 @@ jobs:
|
||||
target: aarch64-apple-darwin
|
||||
artifact: zeroclaw
|
||||
ext: tar.gz
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
artifact: zeroclaw
|
||||
ext: tar.gz
|
||||
ndk: true
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact: zeroclaw.exe
|
||||
@@ -104,6 +199,10 @@ jobs:
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y ${{ matrix.cross_compiler }}
|
||||
|
||||
- name: Setup Android NDK
|
||||
if: matrix.ndk
|
||||
run: echo "$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Build release
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -132,7 +231,7 @@ jobs:
|
||||
|
||||
publish:
|
||||
name: Publish Beta Release
|
||||
needs: [version, build]
|
||||
needs: [version, release-notes, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
@@ -148,23 +247,50 @@ jobs:
|
||||
find . -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
with:
|
||||
tag_name: ${{ needs.version.outputs.tag }}
|
||||
name: ${{ needs.version.outputs.tag }}
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/**/*
|
||||
- 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/ \;
|
||||
cp install.sh release-assets/
|
||||
echo "--- Assets ---"
|
||||
ls -lh release-assets/
|
||||
|
||||
- name: Write release notes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
NOTES: ${{ needs.release-notes.outputs.notes }}
|
||||
run: printf '%s\n' "$NOTES" > release-notes.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAG: ${{ needs.version.outputs.tag }}
|
||||
run: |
|
||||
gh release create "$TAG" release-assets/* \
|
||||
--repo "${{ github.repository }}" \
|
||||
--title "$TAG" \
|
||||
--notes-file release-notes.md \
|
||||
--prerelease
|
||||
|
||||
redeploy-website:
|
||||
name: Trigger Website Redeploy
|
||||
needs: [publish]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger website redeploy
|
||||
env:
|
||||
PAT: ${{ secrets.WEBSITE_REPO_PAT }}
|
||||
run: |
|
||||
curl -fsSL -X POST \
|
||||
-H "Authorization: token $PAT" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
https://api.github.com/repos/zeroclaw-labs/zeroclaw-website/dispatches \
|
||||
-d '{"event_type":"new-release","client_payload":{"install_script_url":"https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh"}}'
|
||||
|
||||
docker:
|
||||
name: Push Docker Image
|
||||
needs: [version, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
@@ -187,3 +313,13 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# ── Post-publish: only run after ALL artifacts are live ──────────────
|
||||
tweet:
|
||||
name: Tweet Release
|
||||
needs: [version, publish, docker, redeploy-website]
|
||||
uses: ./.github/workflows/tweet-release.yml
|
||||
with:
|
||||
release_tag: ${{ needs.version.outputs.tag }}
|
||||
release_url: https://github.com/zeroclaw-labs/zeroclaw/releases/tag/${{ needs.version.outputs.tag }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -74,6 +74,79 @@ jobs:
|
||||
path: web/dist/
|
||||
retention-days: 1
|
||||
|
||||
release-notes:
|
||||
name: Generate Release Notes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
notes: ${{ steps.notes.outputs.body }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build release notes
|
||||
id: notes
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Find the previous stable tag (exclude beta tags)
|
||||
PREV_TAG=$(git tag --sort=-creatordate | grep -vE '\-beta\.' | grep -v "^v${INPUT_VERSION}$" | head -1 || echo "")
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
RANGE="HEAD"
|
||||
else
|
||||
RANGE="${PREV_TAG}..HEAD"
|
||||
fi
|
||||
|
||||
# Extract features only — skip bug fixes for clean release notes
|
||||
FEATURES=$(git log "$RANGE" --pretty=format:"%s" --no-merges \
|
||||
| grep -iE '^feat(\(|:)' \
|
||||
| sed 's/^feat(\([^)]*\)): /\1: /' \
|
||||
| sed 's/^feat: //' \
|
||||
| sed 's/ (#[0-9]*)$//' \
|
||||
| sort -uf \
|
||||
| while IFS= read -r line; do echo "- ${line}"; done || true)
|
||||
|
||||
if [ -z "$FEATURES" ]; then
|
||||
FEATURES="- Incremental improvements and polish"
|
||||
fi
|
||||
|
||||
# Collect ALL unique contributors: git authors + Co-Authored-By
|
||||
GIT_AUTHORS=$(git log "$RANGE" --pretty=format:"%an" --no-merges | sort -uf || true)
|
||||
CO_AUTHORS=$(git log "$RANGE" --pretty=format:"%b" --no-merges \
|
||||
| grep -ioE 'Co-Authored-By: *[^<]+' \
|
||||
| sed 's/Co-Authored-By: *//i' \
|
||||
| sed 's/ *$//' \
|
||||
| sort -uf || true)
|
||||
|
||||
# Merge, deduplicate, and filter out bots
|
||||
ALL_CONTRIBUTORS=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \
|
||||
| sort -uf \
|
||||
| grep -v '^$' \
|
||||
| grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \
|
||||
| while IFS= read -r name; do echo "- ${name}"; done || true)
|
||||
|
||||
BODY=$(cat <<NOTES_EOF
|
||||
## What's New
|
||||
|
||||
${FEATURES}
|
||||
|
||||
## Contributors
|
||||
|
||||
${ALL_CONTRIBUTORS}
|
||||
|
||||
---
|
||||
*Full changelog: ${PREV_TAG}...v${INPUT_VERSION}*
|
||||
NOTES_EOF
|
||||
)
|
||||
|
||||
{
|
||||
echo "body<<BODY_EOF"
|
||||
echo "$BODY"
|
||||
echo "BODY_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
needs: [validate, web]
|
||||
@@ -98,6 +171,11 @@ jobs:
|
||||
target: aarch64-apple-darwin
|
||||
artifact: zeroclaw
|
||||
ext: tar.gz
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
artifact: zeroclaw
|
||||
ext: tar.gz
|
||||
ndk: true
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact: zeroclaw.exe
|
||||
@@ -122,6 +200,10 @@ jobs:
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y ${{ matrix.cross_compiler }}
|
||||
|
||||
- name: Setup Android NDK
|
||||
if: matrix.ndk
|
||||
run: echo "$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Build release
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -150,7 +232,7 @@ jobs:
|
||||
|
||||
publish:
|
||||
name: Publish Stable Release
|
||||
needs: [validate, build]
|
||||
needs: [validate, release-notes, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
@@ -166,23 +248,89 @@ jobs:
|
||||
find . -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
with:
|
||||
tag_name: ${{ needs.validate.outputs.tag }}
|
||||
name: ${{ needs.validate.outputs.tag }}
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/**/*
|
||||
- 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/ \;
|
||||
cp install.sh release-assets/
|
||||
echo "--- Assets ---"
|
||||
ls -lh release-assets/
|
||||
|
||||
- name: Write release notes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NOTES: ${{ needs.release-notes.outputs.notes }}
|
||||
run: printf '%s\n' "$NOTES" > release-notes.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAG: ${{ needs.validate.outputs.tag }}
|
||||
run: |
|
||||
gh release create "$TAG" release-assets/* \
|
||||
--repo "${{ github.repository }}" \
|
||||
--title "$TAG" \
|
||||
--notes-file release-notes.md \
|
||||
--latest
|
||||
|
||||
crates-io:
|
||||
name: Publish to crates.io
|
||||
needs: [validate, publish]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: 1.92.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
|
||||
- name: Build web dashboard
|
||||
run: cd web && npm ci && npm run build
|
||||
|
||||
- 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 to crates.io
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
# Skip if this version is already on crates.io (auto-sync may have published it)
|
||||
CRATE_NAME=$(sed -n 's/^name = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
|
||||
if curl -sfL "https://crates.io/api/v1/crates/${CRATE_NAME}/${VERSION}" | grep -q '"version"'; then
|
||||
echo "::notice::${CRATE_NAME}@${VERSION} already published on crates.io — skipping"
|
||||
else
|
||||
cargo publish --locked --allow-dirty --no-verify
|
||||
fi
|
||||
|
||||
redeploy-website:
|
||||
name: Trigger Website Redeploy
|
||||
needs: [publish]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger website redeploy
|
||||
env:
|
||||
PAT: ${{ secrets.WEBSITE_REPO_PAT }}
|
||||
run: |
|
||||
curl -fsSL -X POST \
|
||||
-H "Authorization: token $PAT" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
https://api.github.com/repos/zeroclaw-labs/zeroclaw-website/dispatches \
|
||||
-d '{"event_type":"new-release","client_payload":{"install_script_url":"https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh"}}'
|
||||
|
||||
docker:
|
||||
name: Push Docker Image
|
||||
needs: [validate, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
@@ -205,3 +353,13 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# ── Post-publish: only run after ALL artifacts are live ──────────────
|
||||
tweet:
|
||||
name: Tweet Release
|
||||
needs: [validate, publish, docker, crates-io, redeploy-website]
|
||||
uses: ./.github/workflows/tweet-release.yml
|
||||
with:
|
||||
release_tag: ${{ needs.validate.outputs.tag }}
|
||||
release_url: https://github.com/zeroclaw-labs/zeroclaw/releases/tag/${{ needs.validate.outputs.tag }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
name: Tweet Release
|
||||
|
||||
on:
|
||||
# Called by release workflows AFTER all publish steps (docker, crates, website) complete.
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: "Release tag (e.g. v0.3.0 or v0.3.0-beta.42)"
|
||||
required: true
|
||||
type: string
|
||||
release_url:
|
||||
description: "GitHub Release URL"
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
TWITTER_CONSUMER_API_KEY:
|
||||
required: false
|
||||
TWITTER_CONSUMER_API_SECRET_KEY:
|
||||
required: false
|
||||
TWITTER_ACCESS_TOKEN:
|
||||
required: false
|
||||
TWITTER_ACCESS_TOKEN_SECRET:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tweet_text:
|
||||
description: "Custom tweet text (include emojis, keep it punchy)"
|
||||
required: true
|
||||
type: string
|
||||
image_url:
|
||||
description: "Optional image URL to attach (png/jpg)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
tweet:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for new features
|
||||
id: check
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag || '' }}
|
||||
MANUAL_TEXT: ${{ inputs.tweet_text || '' }}
|
||||
run: |
|
||||
# Manual dispatch always proceeds
|
||||
if [ -n "$MANUAL_TEXT" ]; then
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find the PREVIOUS release tag (including betas) to check for new features
|
||||
PREV_TAG=$(git tag --sort=-creatordate \
|
||||
| grep -v "^${RELEASE_TAG}$" \
|
||||
| head -1 || echo "")
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count new feat() commits since the previous release
|
||||
NEW_FEATS=$(git log "${PREV_TAG}..${RELEASE_TAG}" --pretty=format:"%s" --no-merges \
|
||||
| grep -ciE '^feat(\(|:)' || echo "0")
|
||||
|
||||
if [ "$NEW_FEATS" -eq 0 ]; then
|
||||
echo "No new features since ${PREV_TAG} — skipping tweet"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "${NEW_FEATS} new feature(s) since ${PREV_TAG} — tweeting"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build tweet text
|
||||
id: tweet
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag || '' }}
|
||||
RELEASE_URL: ${{ inputs.release_url || '' }}
|
||||
MANUAL_TEXT: ${{ inputs.tweet_text || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "$MANUAL_TEXT" ]; then
|
||||
TWEET="$MANUAL_TEXT"
|
||||
else
|
||||
# For features: diff against the PREVIOUS release (including betas)
|
||||
# This prevents duplicate feature lists across consecutive betas
|
||||
PREV_RELEASE=$(git tag --sort=-creatordate \
|
||||
| grep -v "^${RELEASE_TAG}$" \
|
||||
| head -1 || echo "")
|
||||
|
||||
# For contributors: diff against the last STABLE release
|
||||
# This captures everyone across the full release cycle
|
||||
PREV_STABLE=$(git tag --sort=-creatordate \
|
||||
| grep -v "^${RELEASE_TAG}$" \
|
||||
| grep -vE '\-beta\.' \
|
||||
| head -1 || echo "")
|
||||
|
||||
FEAT_RANGE="${PREV_RELEASE:+${PREV_RELEASE}..}${RELEASE_TAG}"
|
||||
CONTRIB_RANGE="${PREV_STABLE:+${PREV_STABLE}..}${RELEASE_TAG}"
|
||||
|
||||
# Extract NEW features only since the last release
|
||||
FEATURES=$(git log "$FEAT_RANGE" --pretty=format:"%s" --no-merges \
|
||||
| grep -iE '^feat(\(|:)' \
|
||||
| sed 's/^feat(\([^)]*\)): /\1: /' \
|
||||
| sed 's/^feat: //' \
|
||||
| sed 's/ (#[0-9]*)$//' \
|
||||
| sort -uf \
|
||||
| head -4 \
|
||||
| while IFS= read -r line; do echo "🚀 ${line}"; done || true)
|
||||
|
||||
if [ -z "$FEATURES" ]; then
|
||||
FEATURES="🚀 Incremental improvements and polish"
|
||||
fi
|
||||
|
||||
# Count ALL contributors across the full release cycle
|
||||
GIT_AUTHORS=$(git log "$CONTRIB_RANGE" --pretty=format:"%an" --no-merges | sort -uf || true)
|
||||
CO_AUTHORS=$(git log "$CONTRIB_RANGE" --pretty=format:"%b" --no-merges \
|
||||
| grep -ioE 'Co-Authored-By: *[^<]+' \
|
||||
| sed 's/Co-Authored-By: *//i' \
|
||||
| sed 's/ *$//' \
|
||||
| sort -uf || true)
|
||||
|
||||
TOTAL_COUNT=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \
|
||||
| sort -uf \
|
||||
| grep -v '^$' \
|
||||
| grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \
|
||||
| grep -c . || echo "0")
|
||||
|
||||
# Build tweet — new features, contributor count, hashtags
|
||||
TWEET=$(printf "🦀 ZeroClaw %s\n\n%s\n\n🙌 %s contributors\n\n%s\n\n#zeroclaw #rust #ai #opensource" \
|
||||
"$RELEASE_TAG" "$FEATURES" "$TOTAL_COUNT" "$RELEASE_URL")
|
||||
fi
|
||||
|
||||
# X/Twitter counts any URL as 23 chars (t.co shortening).
|
||||
# Extract the URL (if present), truncate the BODY to fit, then
|
||||
# re-append the URL so it is never chopped.
|
||||
URL=""
|
||||
BODY="$TWEET"
|
||||
|
||||
# Pull URL out of existing tweet text or use RELEASE_URL
|
||||
FOUND_URL=$(echo "$TWEET" | grep -oE 'https?://[^ ]+' | tail -1 || true)
|
||||
if [ -n "$FOUND_URL" ]; then
|
||||
URL="$FOUND_URL"
|
||||
BODY=$(echo "$TWEET" | sed "s|${URL}||" | sed -e 's/[[:space:]]*$//')
|
||||
elif [ -n "$RELEASE_URL" ]; then
|
||||
URL="$RELEASE_URL"
|
||||
fi
|
||||
|
||||
if [ -n "$URL" ]; then
|
||||
# URL counts as 23 chars on X + 2 chars for \n\n separator = 25
|
||||
MAX_BODY=$((280 - 25))
|
||||
if [ ${#BODY} -gt $MAX_BODY ]; then
|
||||
BODY="${BODY:0:$((MAX_BODY - 3))}..."
|
||||
fi
|
||||
TWEET=$(printf "%s\n\n%s" "$BODY" "$URL")
|
||||
else
|
||||
if [ ${#TWEET} -gt 280 ]; then
|
||||
TWEET="${TWEET:0:277}..."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "--- Tweet preview ---"
|
||||
echo "$TWEET"
|
||||
echo "--- ${#TWEET} chars ---"
|
||||
|
||||
{
|
||||
echo "text<<TWEET_EOF"
|
||||
echo "$TWEET"
|
||||
echo "TWEET_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check for duplicate tweet
|
||||
id: dedup
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
TWEET_TEXT: ${{ steps.tweet.outputs.text }}
|
||||
run: |
|
||||
# Hash the tweet content (ignore whitespace differences)
|
||||
TWEET_HASH=$(echo "$TWEET_TEXT" | tr -s '[:space:]' | sha256sum | cut -d' ' -f1)
|
||||
echo "hash=${TWEET_HASH}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Check if we already have a cache hit for this exact tweet
|
||||
MARKER_FILE="/tmp/tweet-dedup-${TWEET_HASH}"
|
||||
echo "$TWEET_HASH" > "$MARKER_FILE"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
id: tweet-cache
|
||||
with:
|
||||
path: /tmp/tweet-dedup-${{ steps.dedup.outputs.hash }}
|
||||
key: tweet-${{ steps.dedup.outputs.hash }}
|
||||
|
||||
- name: Skip duplicate tweet
|
||||
if: steps.check.outputs.skip != 'true' && steps.tweet-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "::warning::Duplicate tweet detected (hash=${{ steps.dedup.outputs.hash }}) — skipping"
|
||||
echo "This exact tweet was already posted in a previous run."
|
||||
|
||||
- name: Post to X
|
||||
if: steps.check.outputs.skip != 'true' && steps.tweet-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
|
||||
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET_KEY }}
|
||||
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
|
||||
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
|
||||
TWEET_TEXT: ${{ steps.tweet.outputs.text }}
|
||||
IMAGE_URL: ${{ inputs.image_url || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Skip if Twitter secrets are not configured
|
||||
if [ -z "$TWITTER_CONSUMER_KEY" ] || [ -z "$TWITTER_ACCESS_TOKEN" ]; then
|
||||
echo "::warning::Twitter secrets not configured — skipping tweet"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pip install requests requests-oauthlib --quiet
|
||||
|
||||
python3 - <<'PYEOF'
|
||||
import os, sys, time
|
||||
from requests_oauthlib import OAuth1Session
|
||||
|
||||
consumer_key = os.environ["TWITTER_CONSUMER_KEY"]
|
||||
consumer_secret = os.environ["TWITTER_CONSUMER_SECRET"]
|
||||
access_token = os.environ["TWITTER_ACCESS_TOKEN"]
|
||||
access_token_secret = os.environ["TWITTER_ACCESS_TOKEN_SECRET"]
|
||||
tweet_text = os.environ["TWEET_TEXT"]
|
||||
image_url = os.environ.get("IMAGE_URL", "")
|
||||
|
||||
oauth = OAuth1Session(
|
||||
consumer_key,
|
||||
client_secret=consumer_secret,
|
||||
resource_owner_key=access_token,
|
||||
resource_owner_secret=access_token_secret,
|
||||
)
|
||||
|
||||
media_id = None
|
||||
|
||||
# Upload image if provided
|
||||
if image_url:
|
||||
import requests
|
||||
print(f"Downloading image: {image_url}")
|
||||
img_resp = requests.get(image_url, timeout=30)
|
||||
img_resp.raise_for_status()
|
||||
|
||||
content_type = img_resp.headers.get("content-type", "image/png")
|
||||
init_resp = oauth.post(
|
||||
"https://upload.twitter.com/1.1/media/upload.json",
|
||||
data={
|
||||
"command": "INIT",
|
||||
"total_bytes": len(img_resp.content),
|
||||
"media_type": content_type,
|
||||
},
|
||||
)
|
||||
if init_resp.status_code != 202:
|
||||
print(f"Media INIT failed: {init_resp.status_code} {init_resp.text}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
media_id = init_resp.json()["media_id_string"]
|
||||
|
||||
append_resp = oauth.post(
|
||||
"https://upload.twitter.com/1.1/media/upload.json",
|
||||
data={"command": "APPEND", "media_id": media_id, "segment_index": 0},
|
||||
files={"media_data": img_resp.content},
|
||||
)
|
||||
if append_resp.status_code not in (200, 204):
|
||||
print(f"Media APPEND failed: {append_resp.status_code} {append_resp.text}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
fin_resp = oauth.post(
|
||||
"https://upload.twitter.com/1.1/media/upload.json",
|
||||
data={"command": "FINALIZE", "media_id": media_id},
|
||||
)
|
||||
if fin_resp.status_code not in (200, 201):
|
||||
print(f"Media FINALIZE failed: {fin_resp.status_code} {fin_resp.text}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
state = fin_resp.json().get("processing_info", {}).get("state")
|
||||
while state == "pending" or state == "in_progress":
|
||||
wait = fin_resp.json().get("processing_info", {}).get("check_after_secs", 2)
|
||||
time.sleep(wait)
|
||||
status_resp = oauth.get(
|
||||
"https://upload.twitter.com/1.1/media/upload.json",
|
||||
params={"command": "STATUS", "media_id": media_id},
|
||||
)
|
||||
state = status_resp.json().get("processing_info", {}).get("state")
|
||||
fin_resp = status_resp
|
||||
|
||||
print(f"Image uploaded: media_id={media_id}")
|
||||
|
||||
# Post tweet
|
||||
payload = {"text": tweet_text}
|
||||
if media_id:
|
||||
payload["media"] = {"media_ids": [media_id]}
|
||||
|
||||
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
|
||||
|
||||
if resp.status_code == 201:
|
||||
data = resp.json()
|
||||
tweet_id = data["data"]["id"]
|
||||
print(f"Tweet posted: https://x.com/zeroclawlabs/status/{tweet_id}")
|
||||
else:
|
||||
print(f"Failed to post tweet: {resp.status_code}", file=sys.stderr)
|
||||
print(resp.text, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
+4
-1
@@ -43,4 +43,7 @@ credentials.json
|
||||
lcov.info
|
||||
|
||||
# IDE's stuff
|
||||
.idea
|
||||
.idea
|
||||
|
||||
# Wrangler cache
|
||||
.wrangler/
|
||||
Generated
+54
-55
@@ -117,9 +117,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -132,15 +132,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
@@ -752,9 +752,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
version = "1.2.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -889,9 +889,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.60"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -899,9 +899,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.60"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -911,18 +911,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.66"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031"
|
||||
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -932,9 +932,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
@@ -957,9 +957,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
@@ -989,13 +989,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.2"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4"
|
||||
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width 0.2.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -3985,9 +3984,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@@ -5737,9 +5736,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.17.0"
|
||||
version = "3.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
|
||||
checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
@@ -6194,9 +6193,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
@@ -6585,9 +6584,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
@@ -7923,8 +7922,30 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroclaw"
|
||||
version = "0.1.9"
|
||||
name = "zeroclaw-robot-kit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"chrono",
|
||||
"directories",
|
||||
"portable-atomic",
|
||||
"reqwest",
|
||||
"rppal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"toml 1.0.6+spec-1.1.0",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroclawlabs"
|
||||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-imap",
|
||||
@@ -8012,28 +8033,6 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroclaw-robot-kit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"chrono",
|
||||
"directories",
|
||||
"portable-atomic",
|
||||
"reqwest",
|
||||
"rppal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"toml 1.0.6+spec-1.1.0",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.42"
|
||||
|
||||
+20
-2
@@ -3,8 +3,8 @@ members = [".", "crates/robot-kit"]
|
||||
resolver = "2"
|
||||
|
||||
[package]
|
||||
name = "zeroclaw"
|
||||
version = "0.1.9"
|
||||
name = "zeroclawlabs"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
authors = ["theonlyhennygod"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -15,6 +15,24 @@ keywords = ["ai", "agent", "cli", "assistant", "chatbot"]
|
||||
categories = ["command-line-utilities", "api-bindings"]
|
||||
rust-version = "1.87"
|
||||
|
||||
[[bin]]
|
||||
name = "zeroclaw"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "zeroclaw"
|
||||
path = "src/lib.rs"
|
||||
|
||||
include = [
|
||||
"/src/**/*",
|
||||
"/build.rs",
|
||||
"/Cargo.toml",
|
||||
"/Cargo.lock",
|
||||
"/LICENSE*",
|
||||
"/README.md",
|
||||
"/web/dist/**/*",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
# CLI - minimal and fast
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
@@ -18,6 +18,7 @@ COPY crates/robot-kit/Cargo.toml crates/robot-kit/Cargo.toml
|
||||
# Create dummy targets declared in Cargo.toml so manifest parsing succeeds.
|
||||
RUN mkdir -p src benches crates/robot-kit/src \
|
||||
&& echo "fn main() {}" > src/main.rs \
|
||||
&& echo "" > src/lib.rs \
|
||||
&& echo "fn main() {}" > benches/agent_benchmarks.rs \
|
||||
&& echo "pub fn placeholder() {}" > crates/robot-kit/src/lib.rs
|
||||
RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@
|
||||
|
||||
<p align="center"><code>بنية قائمة على السمات · وقت تشغيل آمن افتراضيًا · موفر/قناة/أداة قابلة للتبديل · كل شيء قابل للتوصيل</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 الإعلانات
|
||||
|
||||
استخدم هذا الجدول للإشعارات المهمة (تغييرات التوافق، إشعارات الأمان، نوافذ الصيانة، وحجوز الإصدارات).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # عرض الإصدار ومعلومات البنا
|
||||
|
||||
راجع [مرجع الأوامر](docs/commands-reference.md) للخيارات والأمثلة الكاملة.
|
||||
|
||||
## البنية
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ القنوات (سمة) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ منسق الوكيل │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ توجيه │ │ السياق │ │ التنفيذ │ │
|
||||
│ │ الرسائل │ │ الذاكرة │ │ الأداة │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ الموفرون │ │ الذاكرة │ │ الأدوات │
|
||||
│ (سمة) │ │ (سمة) │ │ (سمة) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ وقت التشغيل (سمة) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**المبادئ الأساسية:**
|
||||
|
||||
- كل شيء هو **سمة** — الموفرون والقنوات والأدوات والذاكرة والأنفاق
|
||||
- القنوات تستدعي المنسق؛ المنسق يستدعي الموفرون + الأدوات
|
||||
- نظام الذاكرة يدير سياق المحادثة (markdown أو SQLite أو لا شيء)
|
||||
- وقت التشغيل يجرد تنفيذ الكود (أصلي أو Docker)
|
||||
- لا قفل للمورد — استبدل Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama بدون تغييرات في الكود
|
||||
|
||||
راجع [توثيق البنية](docs/architecture.svg) للرسوم البيانية التفصيلية وتفاصيل التنفيذ.
|
||||
|
||||
## الأمثلة
|
||||
|
||||
### بوت Telegram
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # معرف مستخدم Telegram الخاص بك
|
||||
```
|
||||
|
||||
ابدأ البرنامج الخفي + الوكيل، ثم أرسل رسالة إلى بوتك على Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
مرحباً! هل يمكنك مساعدتي في كتابة نص Python؟
|
||||
```
|
||||
|
||||
يستجيب البوت بكود مُنشأ بالذكاء الاصطناعي، وينفذ الأدوات إذا طُلب، ويحافظ على سياق المحادثة.
|
||||
|
||||
### Matrix (تشفير من طرف إلى طرف)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
ادعُ `@zeroclaw:matrix.org` إلى غرفة مشفرة، وسيستجيب البوت بتشفير كامل. راجع [دليل Matrix E2EE](docs/matrix-e2ee-guide.md) لإعداد التحقق من الجهاز.
|
||||
|
||||
### متعدد الموفرون
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # التبديل عند خطأ المورد
|
||||
```
|
||||
|
||||
إذا فشل Anthropic أو وصل إلى حد السرعة، يتبادل المنسق تلقائيًا إلى OpenAI.
|
||||
|
||||
### ذاكرة مخصصة
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # حذف تلقائي بعد 90 يومًا
|
||||
```
|
||||
|
||||
أو استخدم Markdown للتخزين القابل للقراءة البشرية:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
راجع [مرجع التكوين](docs/config-reference.md#memory) لجميع خيارات الذاكرة.
|
||||
|
||||
## دعم الموفرون
|
||||
|
||||
| المورد | الحالة | مفتاح API | النماذج المثال |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ مستقر | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ مستقر | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ مستقر | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ مستقر | N/A (محلي) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ مستقر | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ مستقر | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 مخطط | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 مخطط | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### نقاط النهاية المخصصة
|
||||
|
||||
يدعم ZeroClaw نقاط النهاية المتوافقة مع OpenAI:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
مثال: استخدم [LiteLLM](https://github.com/BerriAI/litellm) كوكيل للوصول إلى أي LLM عبر واجهة OpenAI.
|
||||
|
||||
راجع [مرجع الموفرون](docs/providers-reference.md) لتفاصيل التكوين الكاملة.
|
||||
|
||||
## دعم القنوات
|
||||
|
||||
| القناة | الحالة | المصادقة | ملاحظات |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ مستقر | رمز البوت | دعم كامل بما في ذلك الملفات والصور والأزرار المضمنة |
|
||||
| **Matrix** | ✅ مستقر | كلمة المرور أو الرمز | دعم E2EE مع التحقق من الجهاز |
|
||||
| **Slack** | 🚧 مخطط | OAuth أو رمز البوت | يتطلب الوصول إلى مساحة العمل |
|
||||
| **Discord** | 🚧 مخطط | رمز البوت | يتطلب أذونات النقابة |
|
||||
| **WhatsApp** | 🚧 مخطط | Twilio أو API الرسمية | يتطلب حساب تجاري |
|
||||
| **CLI** | ✅ مستقر | لا شيء | واجهة محادثة مباشرة |
|
||||
| **Web** | 🚧 مخطط | مفتاح API أو OAuth | واجهة دردشة قائمة على المتصفح |
|
||||
|
||||
راجع [مرجع القنوات](docs/channels-reference.md) لتعليمات التكوين الكاملة.
|
||||
|
||||
## دعم الأدوات
|
||||
|
||||
يوفر ZeroClaw أدوات مدمجة لتنفيذ الكود والوصول إلى نظام الملفات واسترجاع الويب:
|
||||
|
||||
| الأداة | الوصف | وقت التشغيل المطلوب |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | ينفذ أوامر الصدفة | أصلي أو Docker |
|
||||
| **python** | ينفذ نصوص Python | Python 3.8+ (أصلي) أو Docker |
|
||||
| **javascript** | ينفذ كود Node.js | Node.js 18+ (أصلي) أو Docker |
|
||||
| **filesystem_read** | يقرأ الملفات | أصلي أو Docker |
|
||||
| **filesystem_write** | يكتب الملفات | أصلي أو Docker |
|
||||
| **web_fetch** | يجلب محتوى الويب | أصلي أو Docker |
|
||||
|
||||
### أمان التنفيذ
|
||||
|
||||
- **وقت التشغيل الأصلي** — يعمل كعملية مستخدم البرنامج الخفي، وصول كامل لنظام الملفات
|
||||
- **وقت تشغيل Docker** — عزل حاوية كامل، أنظمة ملفات وشبكات منفصلة
|
||||
|
||||
قم بتكوين سياسة التنفيذ في `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # قائمة سماح صريحة
|
||||
```
|
||||
|
||||
راجع [مرجع التكوين](docs/config-reference.md#runtime) لخيارات الأمان الكاملة.
|
||||
|
||||
## النشر
|
||||
|
||||
### النشر المحلي (التطوير)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### نشر الخادم (الإنتاج)
|
||||
|
||||
استخدم systemd لإدارة البرنامج الخفي والوكيل كخدمات:
|
||||
|
||||
```bash
|
||||
# تثبيت الملف الثنائي
|
||||
cargo install --path . --locked
|
||||
|
||||
# تكوين مساحة العمل
|
||||
zeroclaw init
|
||||
|
||||
# إنشاء ملفات خدمة systemd
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# تمكين وبدء الخدمات
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# التحقق من الحالة
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
راجع [دليل نشر الشبكة](docs/network-deployment.md) لتعليمات نشر الإنتاج الكاملة.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# بناء الصورة
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# تشغيل الحاوية
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
راجع [`Dockerfile`](Dockerfile) لتفاصيل البناء وخيارات التكوين.
|
||||
|
||||
### أجهزة الحافة
|
||||
|
||||
تم تصميم ZeroClaw للعمل على أجهزة منخفضة الطاقة:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 ميغابايت ذاكرة عشوائية، نواة ARMv8 واحدة، < $5 تكلفة الأجهزة
|
||||
- **Raspberry Pi 4/5** — 1 غيغابايت+ ذاكرة عشوائية، متعدد النوى، مثالي لأحمال العمل المتزامنة
|
||||
- **Orange Pi Zero 2** — ~512 ميغابايت ذاكرة عشوائية، رباعي النواة ARMv8، تكلفة منخفضة جدًا
|
||||
- **أجهزة SBCs x86 (Intel N100)** — 4-8 غيغابايت ذاكرة عشوائية، بناء سريع، دعم Docker أصلي
|
||||
|
||||
راجع [دليل الأجهزة](docs/hardware/README.md) لتعليمات الإعداد الخاصة بالجهاز.
|
||||
|
||||
## الأنفاق (التعرض العام)
|
||||
|
||||
اعرض البرنامج الخفي ZeroClaw المحلي الخاص بك للشبكة العامة عبر أنفاق آمنة:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
موفرو الأنفاق المدعومون:
|
||||
|
||||
- **Cloudflare Tunnel** — HTTPS مجاني، لا تعرض للمنافذ، دعم متعدد المجالات
|
||||
- **Ngrok** — إعداد سريع، مجالات مخصصة (خطة مدفوعة)
|
||||
- **Tailscale** — شبكة شبكية خاصة، لا منفذ عام
|
||||
|
||||
راجع [مرجع التكوين](docs/config-reference.md#tunnel) لخيارات التكوين الكاملة.
|
||||
|
||||
## الأمان
|
||||
|
||||
ينفذ ZeroClaw طبقات متعددة من الأمان:
|
||||
|
||||
### الاقتران
|
||||
|
||||
يُنشئ البرنامج الخفي سر اقتران عند التشغيل الأول مخزن في `~/.zeroclaw/workspace/.pairing`. يجب على العملاء (الوكيل، CLI) تقديم هذا السر للاتصال.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # يُنشئ سرًا جديدًا ويبطل القديم
|
||||
```
|
||||
|
||||
### الصندوق الرملي
|
||||
|
||||
- **وقت تشغيل Docker** — عزل حاوية كامل مع أنظمة ملفات وشبكات منفصلة
|
||||
- **وقت التشغيل الأصلي** — يعمل كعملية مستخدم، محدد النطاق في مساحة العمل افتراضيًا
|
||||
|
||||
### قوائم السماح
|
||||
|
||||
يمكن للقنوات تقييد الوصول حسب معرف المستخدم:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # قائمة سماح صريحة
|
||||
```
|
||||
|
||||
### التشفير
|
||||
|
||||
- **Matrix E2EE** — تشفير من طرف إلى طرف كامل مع التحقق من الجهاز
|
||||
- **نقل TLS** — جميع حركة API والنفق تستخدم HTTPS/TLS
|
||||
|
||||
راجع [توثيق الأمان](docs/security/README.md) للسياسات والممارسات الكاملة.
|
||||
|
||||
## إمكانية الملاحظة
|
||||
|
||||
يسجل ZeroClaw في `~/.zeroclaw/workspace/logs/` افتراضيًا. يتم تخزين السجلات حسب المكون:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # سجلات البرنامج الخفي (بدء التشغيل، طلبات API، الأخطاء)
|
||||
├── agent.log # سجلات الوكيل (توجيه الرسائل، تنفيذ الأدوات)
|
||||
├── telegram.log # سجلات خاصة بالقناة (إذا مُكنت)
|
||||
└── matrix.log # سجلات خاصة بالقناة (إذا مُكنت)
|
||||
```
|
||||
|
||||
### تكوين التسجيل
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug، info، warn، error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # يومي، ساعي، حجم
|
||||
max_size_mb = 100 # للتدوير القائم على الحجم
|
||||
retention_days = 30 # حذف تلقائي بعد N يومًا
|
||||
```
|
||||
|
||||
راجع [مرجع التكوين](docs/config-reference.md#logging) لجميع خيارات التسجيل.
|
||||
|
||||
### المقاييس (مخطط)
|
||||
|
||||
دعم مقاييس Prometheus لمراقبة الإنتاج قريبًا. التتبع في [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## المهارات
|
||||
|
||||
يدعم ZeroClaw المهارات المخصصة — وحدات قابلة لإعادة الاستخدام توسع قدرات النظام.
|
||||
|
||||
### تعريف المهارة
|
||||
|
||||
يتم تخزين المهارات في `~/.zeroclaw/workspace/skills/<skill-name>/` بهذا الهيكل:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # بيانات المهارة (الاسم، الوصف، التبعيات)
|
||||
├── prompt.md # موجه النظام للذكاء الاصطناعي
|
||||
└── tools/ # أدوات مخصصة اختيارية
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### مثال المهارة
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "يبحث في الويب ويلخص النتائج"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
أنت مساعد بحث. عند طلب البحث عن شيء ما:
|
||||
|
||||
1. استخدم web_fetch لاسترجاع المحتوى
|
||||
2. لخص النتائج بتنسيق سهل القراءة
|
||||
3. استشهد بالمصادر مع عناوين URL
|
||||
```
|
||||
|
||||
### استخدام المهارات
|
||||
|
||||
يتم تحميل المهارات تلقائيًا عند بدء تشغيل الوكيل. أشر إليها بالاسم في المحادثات:
|
||||
|
||||
```
|
||||
المستخدم: استخدم مهارة البحث على الويب للعثور على أخبار الذكاء الاصطناعي الأخيرة
|
||||
البوت: [يحمل مهارة البحث على الويب، ينفذ web_fetch، يلخص النتائج]
|
||||
```
|
||||
|
||||
راجع قسم [المهارات](#المهارات) لتعليمات إنشاء المهارات الكاملة.
|
||||
|
||||
## المهارات المفتوحة
|
||||
|
||||
يدعم ZeroClaw [Open Skills](https://github.com/openagents-com/open-skills) — نظام معياري ومحايد للمورد لتوسيع قدرات وكلاء الذكاء الاصطناعي.
|
||||
|
||||
### تمكين المهارات المفتوحة
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # اختياري
|
||||
```
|
||||
|
||||
يمكنك أيضًا التجاوز في وقت التشغيل باستخدام `ZEROCLAW_OPEN_SKILLS_ENABLED` و `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## التطوير
|
||||
|
||||
```bash
|
||||
cargo build # بناء التطوير
|
||||
cargo build --release # بناء الإصدار (codegen-units=1، يعمل على جميع الأجهزة بما في ذلك Raspberry Pi)
|
||||
cargo build --profile release-fast # بناء أسرع (codegen-units=8، يتطلب 16 غيغابايت+ ذاكرة عشوائية)
|
||||
cargo test # تشغيل مجموعة الاختبار الكاملة
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # تنسيق
|
||||
|
||||
# تشغيل معيار مقارنة SQLite مقابل Markdown
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### خطاف ما قبل الدفع
|
||||
|
||||
يقوم خطاف git بتشغيل `cargo fmt --check` و `cargo clippy -- -D warnings` و `cargo test` قبل كل دفع. قم بتمكينه مرة واحدة:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### استكشاف أخطاء البناء وإصلاحها (أخطاء OpenSSL على Linux)
|
||||
|
||||
إذا واجهت خطأ بناء `openssl-sys`، قم بمزامنة التبعيات وأعد التجميع باستخدام ملف قفل المستودع:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
تم تكوين ZeroClaw لاستخدام `rustls` لتبعيات HTTP/TLS؛ `--locked` يحافظ على الرسم البياني العابر حتمي في البيئات النظيفة.
|
||||
|
||||
لتخطي الخطاف عندما تحتاج إلى دفع سريع أثناء التطوير:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## التعاون والتوثيق
|
||||
|
||||
ابدأ بمركز التوثيق لخريطة قائمة على المهام:
|
||||
@@ -850,6 +423,20 @@ git push --no-verify
|
||||
|
||||
نحن نبني في المصدر المفتوح لأن أفضل الأفكار تأتي من كل مكان. إذا كنت تقرأ هذا، فأنت جزء منه. مرحبًا. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ المستودع الرسمي وتحذير الانتحال
|
||||
|
||||
**هذا هو مستودع ZeroClaw الرسمي الوحيد:**
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## ZeroClaw কী?
|
||||
|
||||
ZeroClaw হল একটি হালকা, মিউটেবল এবং এক্সটেনসিবল AI অ্যাসিস্ট্যান্ট ইনফ্রাস্ট্রাকচার যা রাস্টে তৈরি। এটি বিভিন্ন LLM প্রদানকারীদের (Anthropic, OpenAI, Google, Ollama, ইত্যাদি) একটি ইউনিফাইড ইন্টারফেসের মাধ্যমে সংযুক্ত করে এবং একাধিক চ্যানেল (Telegram, Matrix, CLI, ইত্যাদি) সমর্থন করে।
|
||||
@@ -177,3 +187,17 @@ channels:
|
||||
যদি ZeroClaw আপনার জন্য উপযোগী হয়, তবে অনুগ্রহ করে আমাদের একটি কফি কিনতে বিবেচনা করুন:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Postaveno studenty a členy komunit Harvard, MIT a Sundai.Club.
|
||||
|
||||
<p align="center"><code>Architektura založená na traitech · bezpečný runtime defaultně · vyměnitelný poskytovatel/kanál/nástroj · vše je připojitelné</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Oznámení
|
||||
|
||||
Použijte tuto tabulku pro důležitá oznámení (změny kompatibility, bezpečnostní upozornění, servisní okna a blokování verzí).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Zobrazuje verzi a build informace
|
||||
|
||||
Viz [Příkazová reference](docs/commands-reference.md) pro kompletní možnosti a příklady.
|
||||
|
||||
## Architektura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Kanály (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Agent Orchestrátor │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Směrování │ │ Kontext │ │ Provedení │ │
|
||||
│ │ Zpráva │ │ Paměť │ │ Nástroj │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Poskytovatel│ │ Paměť │ │ Nástroje │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Klíčové principy:**
|
||||
|
||||
- Vše je **trait** — poskytovatelé, kanály, nástroje, paměť, tunely
|
||||
- Kanály volají orchestrátor; orchestrátor volá poskytovatele + nástroje
|
||||
- Paměťový systém spravuje konverzační kontext (markdown, SQLite, nebo žádný)
|
||||
- Runtime abstrahuje provádění kódu (nativní nebo Docker)
|
||||
- Žádné vendor lock-in — vyměňujte Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama beze změn kódu
|
||||
|
||||
Viz [dokumentace architektury](docs/architecture.svg) pro detailní diagramy a detaily implementace.
|
||||
|
||||
## Příklady
|
||||
|
||||
### Telegram Bot
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Vaše Telegram user ID
|
||||
```
|
||||
|
||||
Spusťte daemon + agent, pak pošlete zprávu vašemu botovi na Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
Ahoj! Mohl bys mi pomoci napsat Python skript?
|
||||
```
|
||||
|
||||
Bot odpoví AI-generovaným kódem, provede nástroje pokud požadováno a udržuje konverzační kontext.
|
||||
|
||||
### Matrix (end-to-end šifrování)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Pozvěte `@zeroclaw:matrix.org` do šifrované místnosti a bot odpoví s plným šifrováním. Viz [Matrix E2EE Guide](docs/matrix-e2ee-guide.md) pro nastavení ověření zařízení.
|
||||
|
||||
### Multi-Poskytovatel
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover při chybě poskytovatele
|
||||
```
|
||||
|
||||
Pokud Anthropic selže nebo má rate-limit, orchestrátor automaticky přepne na OpenAI.
|
||||
|
||||
### Vlastní Paměť
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Automatické čištění po 90 dnech
|
||||
```
|
||||
|
||||
Nebo použijte Markdown pro lidsky čitelné ukládání:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Viz [Konfigurační reference](docs/config-reference.md#memory) pro všechny možnosti paměti.
|
||||
|
||||
## Podpora Poskytovatelů
|
||||
|
||||
| Poskytovatel | Stav | API Klíč | Příklad Modelů |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Stabilní | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Stabilní | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Stabilní | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Stabilní | N/A (lokální) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Stabilní | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Stabilní | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Plánováno | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Plánováno | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Vlastní Endpointy
|
||||
|
||||
ZeroClaw podporuje OpenAI-kompatibilní endpointy:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Příklad: použijte [LiteLLM](https://github.com/BerriAI/litellm) jako proxy pro přístup k jakémukoli LLM přes OpenAI rozhraní.
|
||||
|
||||
Viz [Poskytovatel reference](docs/providers-reference.md) pro kompletní detaily konfigurace.
|
||||
|
||||
## Podpora Kanálů
|
||||
|
||||
| Kanál | Stav | Autentizace | Poznámky |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Stabilní | Bot Token | Plná podpora včetně souborů, obrázků, inline tlačítek |
|
||||
| **Matrix** | ✅ Stabilní | Heslo nebo Token | E2EE podpora s ověřením zařízení |
|
||||
| **Slack** | 🚧 Plánováno | OAuth nebo Bot Token | Vyžaduje workspace přístup |
|
||||
| **Discord** | 🚧 Plánováno | Bot Token | Vyžaduje guild oprávnění |
|
||||
| **WhatsApp** | 🚧 Plánováno | Twilio nebo oficiální API | Vyžaduje business účet |
|
||||
| **CLI** | ✅ Stabilní | Žádné | Přímé konverzační rozhraní |
|
||||
| **Web** | 🚧 Plánováno | API Klíč nebo OAuth | Prohlížečové chat rozhraní |
|
||||
|
||||
Viz [Kanálová reference](docs/channels-reference.md) pro kompletní instrukce konfigurace.
|
||||
|
||||
## Podpora Nástrojů
|
||||
|
||||
ZeroClaw poskytuje vestavěné nástroje pro provádění kódu, přístup k souborovému systému a web retrieval:
|
||||
|
||||
| Nástroj | Popis | Vyžadovaný Runtime |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Provádí shell příkazy | Nativní nebo Docker |
|
||||
| **python** | Provádí Python skripty | Python 3.8+ (nativní) nebo Docker |
|
||||
| **javascript** | Provádí Node.js kód | Node.js 18+ (nativní) nebo Docker |
|
||||
| **filesystem_read** | Čte soubory | Nativní nebo Docker |
|
||||
| **filesystem_write** | Zapisuje soubory | Nativní nebo Docker |
|
||||
| **web_fetch** | Získává web obsah | Nativní nebo Docker |
|
||||
|
||||
### Bezpečnost Provedení
|
||||
|
||||
- **Nativní Runtime** — běží jako uživatelský proces daemon, plný přístup k souborovému systému
|
||||
- **Docker Runtime** — plná kontejnerová izolace, oddělené souborové systémy a sítě
|
||||
|
||||
Nakonfigurujte politiku provedení v `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Explicitní allowlist
|
||||
```
|
||||
|
||||
Viz [Konfigurační reference](docs/config-reference.md#runtime) pro kompletní možnosti bezpečnosti.
|
||||
|
||||
## Nasazení
|
||||
|
||||
### Lokální Nasazení (Vývoj)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Serverové Nasazení (Produkce)
|
||||
|
||||
Použijte systemd pro správu daemon a agent jako služby:
|
||||
|
||||
```bash
|
||||
# Nainstalujte binary
|
||||
cargo install --path . --locked
|
||||
|
||||
# Nakonfigurujte workspace
|
||||
zeroclaw init
|
||||
|
||||
# Vytvořte systemd servisní soubory
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Povolte a spusťte služby
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Ověřte stav
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Viz [Průvodce síťovým nasazením](docs/network-deployment.md) pro kompletní instrukce produkčního nasazení.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Sestavte image
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Spusťte kontejner
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Viz [`Dockerfile`](Dockerfile) pro detaily sestavení a konfigurační možnosti.
|
||||
|
||||
### Edge Hardware
|
||||
|
||||
ZeroClaw je navržen pro běh na nízko-příkonovém hardwaru:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, jedno ARMv8 jádro, < $5 hardwarové náklady
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, vícejádrový, ideální pro souběžné úlohy
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, čtyřjádrový ARMv8, ultra-nízké náklady
|
||||
- **x86 SBCs (Intel N100)** — 4-8 GB RAM, rychlé buildy, nativní Docker podpora
|
||||
|
||||
Viz [Hardware Guide](docs/hardware/README.md) pro instrukce nastavení specifické pro zařízení.
|
||||
|
||||
## Tunneling (Veřejná Expozice)
|
||||
|
||||
Exponujte svůj lokální ZeroClaw daemon do veřejné sítě přes bezpečné tunely:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Podporovaní tunnel poskytovatelé:
|
||||
|
||||
- **Cloudflare Tunnel** — bezplatný HTTPS, bez expozice portů, multi-doména podpora
|
||||
- **Ngrok** — rychlé nastavení, vlastní domény (placený plán)
|
||||
- **Tailscale** — soukromá mesh síť, bez veřejného portu
|
||||
|
||||
Viz [Konfigurační reference](docs/config-reference.md#tunnel) pro kompletní konfigurační možnosti.
|
||||
|
||||
## Bezpečnost
|
||||
|
||||
ZeroClaw implementuje více vrstev bezpečnosti:
|
||||
|
||||
### Párování
|
||||
|
||||
Daemon generuje párovací tajemství při prvním spuštění uložené v `~/.zeroclaw/workspace/.pairing`. Klienti (agent, CLI) musí předložit toto tajemství pro připojení.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Generuje nové tajemství a zneplatňuje staré
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Docker Runtime** — plná kontejnerová izolace s oddělenými souborovými systémy a sítěmi
|
||||
- **Nativní Runtime** — běží jako uživatelský proces, scoped na workspace defaultně
|
||||
|
||||
### Allowlisty
|
||||
|
||||
Kanály mohou omezit přístup podle user ID:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Explicitní allowlist
|
||||
```
|
||||
|
||||
### Šifrování
|
||||
|
||||
- **Matrix E2EE** — plné end-to-end šifrování s ověřením zařízení
|
||||
- **TLS Transport** — veškerý API a tunnel provoz používá HTTPS/TLS
|
||||
|
||||
Viz [Bezpečnostní dokumentace](docs/security/README.md) pro kompletní politiky a praktiky.
|
||||
|
||||
## Pozorovatelnost
|
||||
|
||||
ZeroClaw loguje do `~/.zeroclaw/workspace/logs/` defaultně. Logy jsou ukládány podle komponenty:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Daemon logy (startup, API požadavky, chyby)
|
||||
├── agent.log # Agent logy (směrování zpráv, provedení nástrojů)
|
||||
├── telegram.log # Kanál-specifické logy (pokud povoleno)
|
||||
└── matrix.log # Kanál-specifické logy (pokud povoleno)
|
||||
```
|
||||
|
||||
### Konfigurace Logování
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Pro rotaci založenou na velikosti
|
||||
retention_days = 30 # Automatické čištění po N dnech
|
||||
```
|
||||
|
||||
Viz [Konfigurační reference](docs/config-reference.md#logging) pro všechny možnosti logování.
|
||||
|
||||
### Metriky (Plánováno)
|
||||
|
||||
Podpora Prometheus metrik pro produkční monitoring již brzy. Sledování v [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Dovednosti
|
||||
|
||||
ZeroClaw podporuje vlastní dovednosti — opakovaně použitelné moduly rozšiřující schopnosti systému.
|
||||
|
||||
### Definice Dovednosti
|
||||
|
||||
Dovednosti jsou uloženy v `~/.zeroclaw/workspace/skills/<skill-name>/` s touto strukturou:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Metadata dovednosti (název, popis, závislosti)
|
||||
├── prompt.md # Systémový prompt pro AI
|
||||
└── tools/ # Volitelné vlastní nástroje
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Příklad Dovednosti
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Hledá na webu a shrnuje výsledky"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Jste výzkumný asistent. Když požádáte o výzkum něčeho:
|
||||
|
||||
1. Použijte web_fetch pro získání obsahu
|
||||
2. Shrňte výsledky v snadno čitelném formátu
|
||||
3. Citujte zdroje s URL
|
||||
```
|
||||
|
||||
### Použití Dovedností
|
||||
|
||||
Dovednosti jsou automaticky načítány při startu agenta. Odkazujte na ně jménem v konverzacích:
|
||||
|
||||
```
|
||||
Uživatel: Použij dovednost web-research k nalezení nejnovějších AI zpráv
|
||||
Bot: [načte dovednost web-research, provede web_fetch, shrne výsledky]
|
||||
```
|
||||
|
||||
Viz sekce [Dovednosti](#dovednosti) pro kompletní instrukce tvorby dovedností.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw podporuje [Open Skills](https://github.com/openagents-com/open-skills) — modulární a poskytovatel-agnostický systém pro rozšíření schopností AI agentů.
|
||||
|
||||
### Povolit Open Skills
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # volitelné
|
||||
```
|
||||
|
||||
Můžete také přepsat za běhu pomocí `ZEROCLAW_OPEN_SKILLS_ENABLED` a `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Vývoj
|
||||
|
||||
```bash
|
||||
cargo build # Dev build
|
||||
cargo build --release # Release build (codegen-units=1, funguje na všech zařízeních včetně Raspberry Pi)
|
||||
cargo build --profile release-fast # Rychlejší build (codegen-units=8, vyžaduje 16 GB+ RAM)
|
||||
cargo test # Spustí plnou testovací sadu
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Formátování
|
||||
|
||||
# Spusťte SQLite vs Markdown srovnávací benchmark
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Pre-push hook
|
||||
|
||||
Git hook spouští `cargo fmt --check`, `cargo clippy -- -D warnings`, a `cargo test` před každým push. Povolte jej jednou:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Řešení problémů s Buildem (OpenSSL chyby na Linuxu)
|
||||
|
||||
Pokud narazíte na `openssl-sys` build chybu, synchronizujte závislosti a znovu zkompilujte s lockfile repoziťáře:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw je nakonfigurován pro použití `rustls` pro HTTP/TLS závislosti; `--locked` udržuje transitivní graf deterministický v čistých prostředích.
|
||||
|
||||
Pro přeskočení hooku když potřebujete rychlý push během vývoje:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Spolupráce & Docs
|
||||
|
||||
Začněte s dokumentačním centrem pro task-based mapu:
|
||||
@@ -850,6 +423,20 @@ Upřímné poděkování komunitám a institucím které inspirují a živí tut
|
||||
|
||||
Stavíme v open source protože nejlepší nápady přicházejí odkudkoliv. Pokud toto čtete, jste součástí toho. Vítejte. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Oficiální Repoziťář a Varování před Vydáváním se
|
||||
|
||||
**Toto je jediný oficiální ZeroClaw repoziťář:**
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Hvad er ZeroClaw?
|
||||
|
||||
ZeroClaw er en letvægts, foranderlig og udvidbar AI-assistent-infrastruktur bygget i Rust. Den forbinder forskellige LLM-udbydere (Anthropic, OpenAI, Google, Ollama osv.) via en samlet grænseflade og understøtter flere kanaler (Telegram, Matrix, CLI osv.).
|
||||
@@ -177,3 +187,17 @@ Se [LICENSE-APACHE](LICENSE-APACHE) og [LICENSE-MIT](LICENSE-MIT) for detaljer.
|
||||
Hvis ZeroClaw er nyttigt for dig, overvej venligst at købe os en kaffe:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+24
-437
@@ -90,6 +90,16 @@ Erstellt von Studenten und Mitgliedern der Harvard, MIT und Sundai.Club Gemeinsc
|
||||
|
||||
<p align="center"><code>Trait-basierte Architektur · sicheres Runtime standardmäßig · Provider/Channel/Tool austauschbar · alles ist steckbar</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Ankündigungen
|
||||
|
||||
Verwende diese Tabelle für wichtige Hinweise (Kompatibilitätsänderungen, Sicherheitshinweise, Wartungsfenster und Versionsblockierungen).
|
||||
@@ -367,443 +377,6 @@ zeroclaw version # Zeigt Version und Build-Informationen
|
||||
|
||||
Siehe [Befehlsreferenz](docs/commands-reference.md) für vollständige Optionen und Beispiele.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Channels (Trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Agent-Orchestrator │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Routing │ │ Kontext │ │ Ausführung │ │
|
||||
│ │ Nachricht │ │ Speicher │ │ Werkzeug │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Provider │ │ Speicher │ │ Werkzeuge │
|
||||
│ (Trait) │ │ (Trait) │ │ (Trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (Trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Schlüsselprinzipien:**
|
||||
|
||||
- Alles ist ein **Trait** — Provider, Channels, Tools, Speicher, Tunnel
|
||||
- Channels rufen den Orchestrator auf; der Orchestrator ruft Provider + Tools auf
|
||||
- Das Speichersystem verwaltet Konversationskontext (Markdown, SQLite, oder keiner)
|
||||
- Das Runtime abstrahiert Code-Ausführung (nativ oder Docker)
|
||||
- Kein Provider-Lock-in — tausche Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama ohne Code-Änderungen
|
||||
|
||||
Siehe [Architektur-Dokumentation](docs/architecture.svg) für detaillierte Diagramme und Implementierungsdetails.
|
||||
|
||||
## Beispiele
|
||||
|
||||
### Telegram-Bot
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Deine Telegram-Benutzer-ID
|
||||
```
|
||||
|
||||
Starte den Daemon + Agent, dann sende eine Nachricht an deinen Bot auf Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
Hallo! Könntest du mir helfen, ein Python-Skript zu schreiben?
|
||||
```
|
||||
|
||||
Der Bot antwortet mit KI-generiertem Code, führt Tools auf Anfrage aus und behält den Konversationskontext.
|
||||
|
||||
### Matrix (Ende-zu-Ende-Verschlüsselung)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Lade `@zeroclaw:matrix.org` in einen verschlüsselten Raum ein, und der Bot wird mit vollständiger Verschlüsselung antworten. Siehe [Matrix E2EE-Leitfaden](docs/matrix-e2ee-guide.md) für Geräteverifizierungs-Setup.
|
||||
|
||||
### Multi-Provider
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover bei Provider-Fehler
|
||||
```
|
||||
|
||||
Wenn Anthropic fehlschlägt oder Rate-Limit erreicht, wechselt der Orchestrator automatisch zu OpenAI.
|
||||
|
||||
### Benutzerdefinierter Speicher
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Automatische Bereinigung nach 90 Tagen
|
||||
```
|
||||
|
||||
Oder verwende Markdown für menschenlesbaren Speicher:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Siehe [Konfigurationsreferenz](docs/config-reference.md#memory) für alle Speicheroptionen.
|
||||
|
||||
## Provider-Unterstützung
|
||||
|
||||
| Provider | Status | API-Schlüssel | Beispielmodelle |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Stabil | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Stabil | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Stabil | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Stabil | N/A (lokal) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Stabil | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Stabil | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Geplant | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Geplant | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Benutzerdefinierte Endpoints
|
||||
|
||||
ZeroClaw unterstützt OpenAI-kompatible Endpoints:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Beispiel: verwende [LiteLLM](https://github.com/BerriAI/litellm) als Proxy, um auf jedes LLM über die OpenAI-Schnittstelle zuzugreifen.
|
||||
|
||||
Siehe [Provider-Referenz](docs/providers-reference.md) für vollständige Konfigurationsdetails.
|
||||
|
||||
## Channel-Unterstützung
|
||||
|
||||
| Channel | Status | Authentifizierung | Hinweise |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Stabil | Bot-Token | Vollständige Unterstützung inklusive Dateien, Bilder, Inline-Buttons |
|
||||
| **Matrix** | ✅ Stabil | Passwort oder Token | E2EE-Unterstützung mit Geräteverifizierung |
|
||||
| **Slack** | 🚧 Geplant | OAuth oder Bot-Token | Erfordert Workspace-Zugriff |
|
||||
| **Discord** | 🚧 Geplant | Bot-Token | Erfordert Guild-Berechtigungen |
|
||||
| **WhatsApp** | 🚧 Geplant | Twilio oder offizielle API | Erfordert Business-Konto |
|
||||
| **CLI** | ✅ Stabil | Keine | Direkte konversationelle Schnittstelle |
|
||||
| **Web** | 🚧 Geplant | API-Schlüssel oder OAuth | Browserbasierte Chat-Schnittstelle |
|
||||
|
||||
Siehe [Channel-Referenz](docs/channels-reference.md) für vollständige Konfigurationsanleitungen.
|
||||
|
||||
## Tool-Unterstützung
|
||||
|
||||
ZeroClaw bietet integrierte Tools für Code-Ausführung, Dateisystemzugriff und Web-Abruf:
|
||||
|
||||
| Tool | Beschreibung | Erforderliches Runtime |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Führt Shell-Befehle aus | Nativ oder Docker |
|
||||
| **python** | Führt Python-Skripte aus | Python 3.8+ (nativ) oder Docker |
|
||||
| **javascript** | Führt Node.js-Code aus | Node.js 18+ (nativ) oder Docker |
|
||||
| **filesystem_read** | Liest Dateien | Nativ oder Docker |
|
||||
| **filesystem_write** | Schreibt Dateien | Nativ oder Docker |
|
||||
| **web_fetch** | Ruft Web-Inhalte ab | Nativ oder Docker |
|
||||
|
||||
### Ausführungssicherheit
|
||||
|
||||
- **Natives Runtime** — läuft als Benutzerprozess des Daemons, voller Dateisystemzugriff
|
||||
- **Docker-Runtime** — vollständige Container-Isolierung, separate Dateisysteme und Netzwerke
|
||||
|
||||
Konfiguriere die Ausführungsrichtlinie in `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Explizite Allowlist
|
||||
```
|
||||
|
||||
Siehe [Konfigurationsreferenz](docs/config-reference.md#runtime) für vollständige Sicherheitsoptionen.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Lokales Deployment (Entwicklung)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Server-Deployment (Produktion)
|
||||
|
||||
Verwende systemd, um Daemon und Agent als Dienste zu verwalten:
|
||||
|
||||
```bash
|
||||
# Installiere das Binary
|
||||
cargo install --path . --locked
|
||||
|
||||
# Konfiguriere den Workspace
|
||||
zeroclaw init
|
||||
|
||||
# Erstelle systemd-Dienstdateien
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Aktiviere und starte die Dienste
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Überprüfe den Status
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Siehe [Netzwerk-Deployment-Leitfaden](docs/network-deployment.md) für vollständige Produktions-Deployment-Anleitungen.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Baue das Image
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Führe den Container aus
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Siehe [`Dockerfile`](Dockerfile) für Build-Details und Konfigurationsoptionen.
|
||||
|
||||
### Edge-Hardware
|
||||
|
||||
ZeroClaw ist für den Betrieb auf Low-Power-Hardware konzipiert:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, einzelner ARMv8-Kern, < $5 Hardware-Kosten
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, Multi-Core, ideal für gleichzeitige Workloads
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, Quad-Core ARMv8, Ultra-Low-Cost
|
||||
- **x86 SBCs (Intel N100)** — 4-8 GB RAM, schnelle Builds, nativer Docker-Support
|
||||
|
||||
Siehe [Hardware-Leitfaden](docs/hardware/README.md) für gerätespezifische Einrichtungsanleitungen.
|
||||
|
||||
## Tunneling (Öffentliche Exposition)
|
||||
|
||||
Exponiere deinen lokalen ZeroClaw-Daemon über sichere Tunnel zum öffentlichen Netzwerk:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Unterstützte Tunnel-Provider:
|
||||
|
||||
- **Cloudflare Tunnel** — kostenloses HTTPS, keine Port-Exposition, Multi-Domain-Support
|
||||
- **Ngrok** — schnelle Einrichtung, benutzerdefinierte Domains (kostenpflichtiger Plan)
|
||||
- **Tailscale** — privates Mesh-Netzwerk, kein öffentlicher Port
|
||||
|
||||
Siehe [Konfigurationsreferenz](docs/config-reference.md#tunnel) für vollständige Konfigurationsoptionen.
|
||||
|
||||
## Sicherheit
|
||||
|
||||
ZeroClaw implementiert mehrere Sicherheitsebenen:
|
||||
|
||||
### Pairing
|
||||
|
||||
Der Daemon generiert beim ersten Start ein Pairing-Geheimnis, das in `~/.zeroclaw/workspace/.pairing` gespeichert wird. Clients (Agent, CLI) müssen dieses Geheimnis präsentieren, um eine Verbindung herzustellen.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Generiert ein neues Geheimnis und erklärt das alte für ungültig
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Docker-Runtime** — vollständige Container-Isolierung mit separaten Dateisystemen und Netzwerken
|
||||
- **Natives Runtime** — läuft als Benutzerprozess, standardmäßig auf Workspace beschränkt
|
||||
|
||||
### Allowlists
|
||||
|
||||
Channels können den Zugriff nach Benutzer-ID einschränken:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Explizite Allowlist
|
||||
```
|
||||
|
||||
### Verschlüsselung
|
||||
|
||||
- **Matrix E2EE** — vollständige Ende-zu-Ende-Verschlüsselung mit Geräteverifizierung
|
||||
- **TLS-Transport** — der gesamte API- und Tunnel-Verkehr verwendet HTTPS/TLS
|
||||
|
||||
Siehe [Sicherheitsdokumentation](docs/security/README.md) für vollständige Richtlinien und Praktiken.
|
||||
|
||||
## Observability
|
||||
|
||||
ZeroClaw protokolliert standardmäßig in `~/.zeroclaw/workspace/logs/`. Logs werden nach Komponente gespeichert:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Daemon-Logs (Start, API-Anfragen, Fehler)
|
||||
├── agent.log # Agent-Logs (Nachrichten-Routing, Tool-Ausführung)
|
||||
├── telegram.log # Kanalspezifische Logs (falls aktiviert)
|
||||
└── matrix.log # Kanalspezifische Logs (falls aktiviert)
|
||||
```
|
||||
|
||||
### Logging-Konfiguration
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Für größenbasierte Rotation
|
||||
retention_days = 30 # Automatische Bereinigung nach N Tagen
|
||||
```
|
||||
|
||||
Siehe [Konfigurationsreferenz](docs/config-reference.md#logging) für alle Logging-Optionen.
|
||||
|
||||
### Metriken (Geplant)
|
||||
|
||||
Prometheus-Metrik-Unterstützung für Produktionsüberwachung kommt bald. Verfolgung in [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Skills
|
||||
|
||||
ZeroClaw unterstützt benutzerdefinierte Skills — wiederverwendbare Module, die die Systemfähigkeiten erweitern.
|
||||
|
||||
### Skill-Definition
|
||||
|
||||
Skills werden in `~/.zeroclaw/workspace/skills/<skill-name>/` mit dieser Struktur gespeichert:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Skill-Metadaten (Name, Beschreibung, Abhängigkeiten)
|
||||
├── prompt.md # System-Prompt für die KI
|
||||
└── tools/ # Optionale benutzerdefinierte Tools
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Skill-Beispiel
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Sucht im Web und fasst Ergebnisse zusammen"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Du bist ein Forschungsassistent. Wenn du gebeten wirst, etwas zu recherchieren:
|
||||
|
||||
1. Verwende web_fetch, um den Inhalt abzurufen
|
||||
2. Fasse die Ergebnisse in einem leicht lesbaren Format zusammen
|
||||
3. Zitiere die Quellen mit URLs
|
||||
```
|
||||
|
||||
### Skill-Verwendung
|
||||
|
||||
Skills werden beim Agent-Start automatisch geladen. Referenziere sie nach Namen in Konversationen:
|
||||
|
||||
```
|
||||
Benutzer: Verwende den Web-Research-Skill, um die neuesten KI-Nachrichten zu finden
|
||||
Bot: [lädt den Web-Research-Skill, führt web_fetch aus, fasst Ergebnisse zusammen]
|
||||
```
|
||||
|
||||
Siehe Abschnitt [Skills](#skills) für vollständige Skill-Erstellungsanleitungen.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw unterstützt [Open Skills](https://github.com/openagents-com/open-skills) — ein modulares und provider-agnostisches System zur Erweiterung von KI-Agenten-Fähigkeiten.
|
||||
|
||||
### Open Skills aktivieren
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # optional
|
||||
```
|
||||
|
||||
Du kannst auch zur Laufzeit mit `ZEROCLAW_OPEN_SKILLS_ENABLED` und `ZEROCLAW_OPEN_SKILLS_DIR` überschreiben.
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
cargo build # Entwicklungs-Build
|
||||
cargo build --release # Release-Build (codegen-units=1, funktioniert auf allen Geräten einschließlich Raspberry Pi)
|
||||
cargo build --profile release-fast # Schnellerer Build (codegen-units=8, erfordert 16 GB+ RAM)
|
||||
cargo test # Führt die vollständige Test-Suite aus
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Formatierung
|
||||
|
||||
# Führe den SQLite vs Markdown Vergleichs-Benchmark aus
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Pre-push-Hook
|
||||
|
||||
Ein Git-Hook führt `cargo fmt --check`, `cargo clippy -- -D warnings`, und `cargo test` vor jedem Push aus. Aktiviere ihn einmal:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Build-Fehlerbehebung (OpenSSL-Fehler unter Linux)
|
||||
|
||||
Wenn du auf einen `openssl-sys`-Build-Fehler stößt, synchronisiere Abhängigkeiten und kompiliere mit dem Lockfile des Repositories neu:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw ist so konfiguriert, dass es `rustls` für HTTP/TLS-Abhängigkeiten verwendet; `--locked` hält den transitiven Graphen in sauberen Umgebungen deterministisch.
|
||||
|
||||
Um den Hook zu überspringen, wenn du während der Entwicklung einen schnellen Push benötigst:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Zusammenarbeit & Docs
|
||||
|
||||
Beginne mit dem Dokumentations-Hub für eine Aufgaben-basierte Karte:
|
||||
@@ -854,6 +427,20 @@ Ein herzliches Dankeschön an die Gemeinschaften und Institutionen, die diese Op
|
||||
|
||||
Wir bauen in Open Source, weil die besten Ideen von überall kommen. Wenn du das liest, bist du Teil davon. Willkommen. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Offizielles Repository und Fälschungswarnung
|
||||
|
||||
**Dies ist das einzige offizielle ZeroClaw-Repository:**
|
||||
|
||||
@@ -54,6 +54,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
> **📝 Σημείωση:** Αυτό είναι ένα συνοπτικό README στα ελληνικά. Για πλήρη τεκμηρίωση, ανατρέξτε στο [αγγλικό README](README.md). Οι σύνδεσμοι τεκμηρίωσης παραπέμπουν στην αγγλική τεκμηρίωση.
|
||||
|
||||
## Τι είναι το ZeroClaw;
|
||||
@@ -176,3 +186,17 @@ channels:
|
||||
Αν το ZeroClaw είναι χρήσιμο για εσάς, παρακαλώ σκεφτείτε να μας αγοράσετε έναν καφέ:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Construido por estudiantes y miembros de las comunidades de Harvard, MIT y Sunda
|
||||
|
||||
<p align="center"><code>Arquitectura basada en traits · runtime seguro por defecto · proveedor/canal/herramienta intercambiables · todo es conectable</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Anuncios
|
||||
|
||||
Usa esta tabla para avisos importantes (cambios de compatibilidad, avisos de seguridad, ventanas de mantenimiento y bloqueos de versión).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Muestra versión e información de build
|
||||
|
||||
Ver [Referencia de Comandos](docs/commands-reference.md) para opciones y ejemplos completos.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Canales (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Orquestador Agent │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Ruteo │ │ Contexto │ │ Ejecución │ │
|
||||
│ │ Mensaje │ │ Memoria │ │ Herramienta│ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Proveedores │ │ Memoria │ │ Herramientas │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Principios clave:**
|
||||
|
||||
- Todo es un **trait** — proveedores, canales, herramientas, memoria, túneles
|
||||
- Los canales llaman al orquestador; el orquestador llama a proveedores + herramientas
|
||||
- El sistema de memoria gestiona contexto conversacional (markdown, SQLite, o ninguno)
|
||||
- El runtime abstrae la ejecución de código (nativo o Docker)
|
||||
- Sin lock-in de proveedor — intercambia Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama sin cambios de código
|
||||
|
||||
Ver [documentación de arquitectura](docs/architecture.svg) para diagramas detallados y detalles de implementación.
|
||||
|
||||
## Ejemplos
|
||||
|
||||
### Bot de Telegram
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Tu ID de usuario de Telegram
|
||||
```
|
||||
|
||||
Inicia el daemon + agent, luego envía un mensaje a tu bot en Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
¡Hola! ¿Podrías ayudarme a escribir un script Python?
|
||||
```
|
||||
|
||||
El bot responde con código generado por AI, ejecuta herramientas si se solicita, y mantiene el contexto de conversación.
|
||||
|
||||
### Matrix (cifrado extremo a extremo)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Invita a `@zeroclaw:matrix.org` a una sala cifrada, y el bot responderá con cifrado completo. Ver [Guía Matrix E2EE](docs/matrix-e2ee-guide.md) para configuración de verificación de dispositivo.
|
||||
|
||||
### Multi-Proveedor
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover en error de proveedor
|
||||
```
|
||||
|
||||
Si Anthropic falla o tiene rate-limit, el orquestador hace failover automáticamente a OpenAI.
|
||||
|
||||
### Memoria Personalizada
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Purga automática después de 90 días
|
||||
```
|
||||
|
||||
O usa Markdown para almacenamiento legible por humanos:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Ver [Referencia de Configuración](docs/config-reference.md#memory) para todas las opciones de memoria.
|
||||
|
||||
## Soporte de Proveedor
|
||||
|
||||
| Proveedor | Estado | API Key | Modelos de Ejemplo |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Estable | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Estable | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Estable | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Estable | N/A (local) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Estable | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Estable | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Planificado | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Planificado | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Endpoints Personalizados
|
||||
|
||||
ZeroClaw soporta endpoints compatibles con OpenAI:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Ejemplo: usa [LiteLLM](https://github.com/BerriAI/litellm) como proxy para acceder a cualquier LLM vía interfaz OpenAI.
|
||||
|
||||
Ver [Referencia de Proveedores](docs/providers-reference.md) para detalles de configuración completos.
|
||||
|
||||
## Soporte de Canal
|
||||
|
||||
| Canal | Estado | Autenticación | Notas |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Estable | Bot Token | Soporte completo incluyendo archivos, imágenes, botones inline |
|
||||
| **Matrix** | ✅ Estable | Contraseña o Token | Soporte E2EE con verificación de dispositivo |
|
||||
| **Slack** | 🚧 Planificado | OAuth o Bot Token | Requiere acceso a workspace |
|
||||
| **Discord** | 🚧 Planificado | Bot Token | Requiere permisos de guild |
|
||||
| **WhatsApp** | 🚧 Planificado | Twilio o API oficial | Requiere cuenta business |
|
||||
| **CLI** | ✅ Estable | Ninguno | Interfaz conversacional directa |
|
||||
| **Web** | 🚧 Planificado | API Key o OAuth | Interfaz de chat basada en navegador |
|
||||
|
||||
Ver [Referencia de Canales](docs/channels-reference.md) para instrucciones de configuración completas.
|
||||
|
||||
## Soporte de Herramientas
|
||||
|
||||
ZeroClaw proporciona herramientas integradas para ejecución de código, acceso al sistema de archivos y recuperación web:
|
||||
|
||||
| Herramienta | Descripción | Runtime Requerido |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Ejecuta comandos shell | Nativo o Docker |
|
||||
| **python** | Ejecuta scripts Python | Python 3.8+ (nativo) o Docker |
|
||||
| **javascript** | Ejecuta código Node.js | Node.js 18+ (nativo) o Docker |
|
||||
| **filesystem_read** | Lee archivos | Nativo o Docker |
|
||||
| **filesystem_write** | Escribe archivos | Nativo o Docker |
|
||||
| **web_fetch** | Obtiene contenido web | Nativo o Docker |
|
||||
|
||||
### Seguridad de Ejecución
|
||||
|
||||
- **Runtime Nativo** — se ejecuta como proceso de usuario del daemon, acceso completo al sistema de archivos
|
||||
- **Runtime Docker** — aislamiento completo de contenedor, sistemas de archivos y redes separados
|
||||
|
||||
Configura la política de ejecución en `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Lista permitida explícita
|
||||
```
|
||||
|
||||
Ver [Referencia de Configuración](docs/config-reference.md#runtime) para opciones de seguridad completas.
|
||||
|
||||
## Despliegue
|
||||
|
||||
### Despliegue Local (Desarrollo)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Despliegue en Servidor (Producción)
|
||||
|
||||
Usa systemd para gestionar el daemon y agent como servicios:
|
||||
|
||||
```bash
|
||||
# Instala el binario
|
||||
cargo install --path . --locked
|
||||
|
||||
# Configura el workspace
|
||||
zeroclaw init
|
||||
|
||||
# Crea archivos de servicio systemd
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Habilita e inicia los servicios
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Verifica el estado
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Ver [Guía de Despliegue de Red](docs/network-deployment.md) para instrucciones completas de despliegue en producción.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Compila la imagen
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Ejecuta el contenedor
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Ver [`Dockerfile`](Dockerfile) para detalles de build y opciones de configuración.
|
||||
|
||||
### Hardware Edge
|
||||
|
||||
ZeroClaw está diseñado para ejecutarse en hardware de bajo consumo:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, núcleo ARMv8 único, < $5 costo de hardware
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, multi-núcleo, ideal para workloads concurrentes
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, quad-core ARMv8, costo ultra-bajo
|
||||
- **SBCs x86 (Intel N100)** — 4-8 GB RAM, builds rápidos, soporte Docker nativo
|
||||
|
||||
Ver [Guía de Hardware](docs/hardware/README.md) para instrucciones de configuración específicas por dispositivo.
|
||||
|
||||
## Tunneling (Exposición Pública)
|
||||
|
||||
Expón tu daemon ZeroClaw local a la red pública vía túneles seguros:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Proveedores de tunnel soportados:
|
||||
|
||||
- **Cloudflare Tunnel** — HTTPS gratis, sin exposición de puertos, soporte multi-dominio
|
||||
- **Ngrok** — configuración rápida, dominios personalizados (plan de pago)
|
||||
- **Tailscale** — red mesh privada, sin puerto público
|
||||
|
||||
Ver [Referencia de Configuración](docs/config-reference.md#tunnel) para opciones de configuración completas.
|
||||
|
||||
## Seguridad
|
||||
|
||||
ZeroClaw implementa múltiples capas de seguridad:
|
||||
|
||||
### Emparejamiento
|
||||
|
||||
El daemon genera un secreto de emparejamiento al primer inicio almacenado en `~/.zeroclaw/workspace/.pairing`. Los clientes (agent, CLI) deben presentar este secreto para conectarse.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Genera un nuevo secreto e invalida el anterior
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Runtime Docker** — aislamiento completo de contenedor con sistemas de archivos y redes separados
|
||||
- **Runtime Nativo** — se ejecuta como proceso de usuario, con alcance de workspace por defecto
|
||||
|
||||
### Listas Permitidas
|
||||
|
||||
Los canales pueden restringir acceso por ID de usuario:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Lista permitida explícita
|
||||
```
|
||||
|
||||
### Cifrado
|
||||
|
||||
- **Matrix E2EE** — cifrado extremo a extremo completo con verificación de dispositivo
|
||||
- **Transporte TLS** — todo el tráfico de API y tunnel usa HTTPS/TLS
|
||||
|
||||
Ver [Documentación de Seguridad](docs/security/README.md) para políticas y prácticas completas.
|
||||
|
||||
## Observabilidad
|
||||
|
||||
ZeroClaw registra logs en `~/.zeroclaw/workspace/logs/` por defecto. Los logs se almacenan por componente:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Logs del daemon (inicio, solicitudes API, errores)
|
||||
├── agent.log # Logs del agent (ruteo de mensajes, ejecución de herramientas)
|
||||
├── telegram.log # Logs específicos del canal (si está habilitado)
|
||||
└── matrix.log # Logs específicos del canal (si está habilitado)
|
||||
```
|
||||
|
||||
### Configuración de Logging
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Para rotación basada en tamaño
|
||||
retention_days = 30 # Purga automática después de N días
|
||||
```
|
||||
|
||||
Ver [Referencia de Configuración](docs/config-reference.md#logging) para todas las opciones de logging.
|
||||
|
||||
### Métricas (Planificado)
|
||||
|
||||
Soporte de métricas Prometheus para monitoreo en producción próximamente. Seguimiento en [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Habilidades (Skills)
|
||||
|
||||
ZeroClaw soporta habilidades personalizadas — módulos reutilizables que extienden las capacidades del sistema.
|
||||
|
||||
### Definición de Habilidad
|
||||
|
||||
Las habilidades se almacenan en `~/.zeroclaw/workspace/skills/<skill-name>/` con esta estructura:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Metadatos de habilidad (nombre, descripción, dependencias)
|
||||
├── prompt.md # Prompt de sistema para la AI
|
||||
└── tools/ # Herramientas personalizadas opcionales
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Ejemplo de Habilidad
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Busca en la web y resume resultados"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Eres un asistente de investigación. Cuando te pidan buscar algo:
|
||||
|
||||
1. Usa web_fetch para obtener el contenido
|
||||
2. Resume los resultados en un formato fácil de leer
|
||||
3. Cita las fuentes con URLs
|
||||
```
|
||||
|
||||
### Uso de Habilidades
|
||||
|
||||
Las habilidades se cargan automáticamente al inicio del agent. Referéncialas por nombre en conversaciones:
|
||||
|
||||
```
|
||||
Usuario: Usa la habilidad web-research para encontrar las últimas noticias de AI
|
||||
Bot: [carga la habilidad web-research, ejecuta web_fetch, resume resultados]
|
||||
```
|
||||
|
||||
Ver sección [Habilidades (Skills)](#habilidades-skills) para instrucciones completas de creación de habilidades.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw soporta [Open Skills](https://github.com/openagents-com/open-skills) — un sistema modular y agnóstico de proveedores para extender capacidades de agentes AI.
|
||||
|
||||
### Habilitar Open Skills
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # opcional
|
||||
```
|
||||
|
||||
También puedes sobrescribir en runtime con `ZEROCLAW_OPEN_SKILLS_ENABLED` y `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Desarrollo
|
||||
|
||||
```bash
|
||||
cargo build # Build de desarrollo
|
||||
cargo build --release # Build release (codegen-units=1, funciona en todos los dispositivos incluyendo Raspberry Pi)
|
||||
cargo build --profile release-fast # Build más rápido (codegen-units=8, requiere 16 GB+ RAM)
|
||||
cargo test # Ejecuta el suite de pruebas completo
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Formato
|
||||
|
||||
# Ejecuta el benchmark de comparación SQLite vs Markdown
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Hook pre-push
|
||||
|
||||
Un hook de git ejecuta `cargo fmt --check`, `cargo clippy -- -D warnings`, y `cargo test` antes de cada push. Actívalo una vez:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Solución de Problemas de Build (errores OpenSSL en Linux)
|
||||
|
||||
Si encuentras un error de build `openssl-sys`, sincroniza dependencias y recompila con el lockfile del repositorio:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw está configurado para usar `rustls` para dependencias HTTP/TLS; `--locked` mantiene el grafo transitivo determinista en entornos limpios.
|
||||
|
||||
Para saltar el hook cuando necesites un push rápido durante desarrollo:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Colaboración y Docs
|
||||
|
||||
Comienza con el hub de documentación para un mapa basado en tareas:
|
||||
@@ -850,6 +423,20 @@ Un sincero agradecimiento a las comunidades e instituciones que inspiran y alime
|
||||
|
||||
Construimos en código abierto porque las mejores ideas vienen de todas partes. Si estás leyendo esto, eres parte de esto. Bienvenido. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Repositorio Oficial y Advertencia de Suplantación
|
||||
|
||||
**Este es el único repositorio oficial de ZeroClaw:**
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Mikä on ZeroClaw?
|
||||
|
||||
ZeroClaw on kevyt, muokattava ja laajennettava AI-assistentti-infrastruktuuri, joka on rakennettu Rustilla. Se yhdistää eri LLM-palveluntarjoajat (Anthropic, OpenAI, Google, Ollama jne.) yhtenäisen käyttöliittymän kautta ja tukee useita kanavia (Telegram, Matrix, CLI jne.).
|
||||
@@ -177,3 +187,17 @@ Katso [LICENSE-APACHE](LICENSE-APACHE) ja [LICENSE-MIT](LICENSE-MIT) yksityiskoh
|
||||
Jos ZeroClaw on hyödyllinen sinulle, harkitse kahvin ostamista meille:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+25
-438
@@ -58,7 +58,7 @@ Construit par des étudiants et membres des communautés Harvard, MIT et Sundai.
|
||||
|
||||
<p align="center">
|
||||
<a href="#démarrage-rapide">Démarrage</a> |
|
||||
<a href="install.sh">Configuration en un clic</a> |
|
||||
<a href="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh">Configuration en un clic</a> |
|
||||
<a href="docs/README.md">Hub Documentation</a> |
|
||||
<a href="docs/SUMMARY.md">Table des matières Documentation</a>
|
||||
</p>
|
||||
@@ -84,6 +84,16 @@ Construit par des étudiants et membres des communautés Harvard, MIT et Sundai.
|
||||
|
||||
<p align="center"><code>Architecture pilotée par traits · runtime sécurisé par défaut · fournisseur/canal/outil interchangeables · tout est pluggable</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Annonces
|
||||
|
||||
Utilisez ce tableau pour les avis importants (changements incompatibles, avis de sécurité, fenêtres de maintenance et bloqueurs de version).
|
||||
@@ -361,443 +371,6 @@ zeroclaw version # Affiche la version et les informations de build
|
||||
|
||||
Voir [Référence des Commandes](docs/reference/cli/commands-reference.md) pour les options et exemples complets.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Canaux (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Orchestrateur Agent │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Routage │ │ Contexte │ │ Exécution │ │
|
||||
│ │ Message │ │ Mémoire │ │ Outil │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Fournisseurs │ │ Mémoire │ │ Outils │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Principes clés :**
|
||||
|
||||
- Tout est un **trait** — fournisseurs, canaux, outils, mémoire, tunnels
|
||||
- Les canaux appellent l'orchestrateur ; l'orchestrateur appelle les fournisseurs + outils
|
||||
- Le système mémoire gère le contexte conversationnel (markdown, SQLite, ou aucun)
|
||||
- Le runtime abstrait l'exécution de code (natif ou Docker)
|
||||
- Aucun verrouillage de fournisseur — échangez Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama sans changement de code
|
||||
|
||||
Voir [documentation architecture](docs/assets/architecture.svg) pour les diagrammes détaillés et les détails d'implémentation.
|
||||
|
||||
## Exemples
|
||||
|
||||
### Telegram Bot
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Votre Telegram user ID
|
||||
```
|
||||
|
||||
Démarrez le daemon + agent, puis envoyez un message à votre bot sur Telegram :
|
||||
|
||||
```
|
||||
/start
|
||||
Bonjour ! Pouvez-vous m'aider à écrire un script Python ?
|
||||
```
|
||||
|
||||
Le bot répond avec le code généré par l'IA, exécute les outils si demandé, et conserve le contexte de conversation.
|
||||
|
||||
### Matrix (chiffré de bout en bout)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Invitez `@zeroclaw:matrix.org` dans une salle chiffrée, et le bot répondra avec le chiffrement complet. Voir [Guide Matrix E2EE](docs/security/matrix-e2ee-guide.md) pour la configuration de vérification de dispositif.
|
||||
|
||||
### Multi-Fournisseur
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Bascule en cas d'erreur du fournisseur
|
||||
```
|
||||
|
||||
Si Anthropic échoue ou rate-limit, l'orchestrateur bascule automatiquement vers OpenAI.
|
||||
|
||||
### Mémoire Personnalisée
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Purge automatique après 90 jours
|
||||
```
|
||||
|
||||
Ou utilisez Markdown pour un stockage lisible par l'humain :
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Voir [Référence de Configuration](docs/reference/api/config-reference.md#memory) pour toutes les options mémoire.
|
||||
|
||||
## Support de Fournisseur
|
||||
|
||||
| Fournisseur | Statut | Clé API | Modèles Exemple |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Stable | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Stable | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Stable | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Stable | N/A (local) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Stable | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Stable | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Planifié | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Planifié | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Endpoints Personnalisés
|
||||
|
||||
ZeroClaw prend en charge les endpoints compatibles OpenAI :
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Exemple : utilisez [LiteLLM](https://github.com/BerriAI/litellm) comme proxy pour accéder à n'importe quel LLM via l'interface OpenAI.
|
||||
|
||||
Voir [Référence des Fournisseurs](docs/reference/api/providers-reference.md) pour les détails de configuration complets.
|
||||
|
||||
## Support de Canal
|
||||
|
||||
| Canal | Statut | Authentification | Notes |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Stable | Bot Token | Support complet incluant fichiers, images, boutons inline |
|
||||
| **Matrix** | ✅ Stable | Mot de passe ou Token | Support E2EE avec vérification de dispositif |
|
||||
| **Slack** | 🚧 Planifié | OAuth ou Bot Token | Accès workspace requis |
|
||||
| **Discord** | 🚧 Planifié | Bot Token | Permissions guild requises |
|
||||
| **WhatsApp** | 🚧 Planifié | Twilio ou API officielle | Compte business requis |
|
||||
| **CLI** | ✅ Stable | Aucun | Interface conversationnelle directe |
|
||||
| **Web** | 🚧 Planifié | Clé API ou OAuth | Interface de chat basée navigateur |
|
||||
|
||||
Voir [Référence des Canaux](docs/reference/api/channels-reference.md) pour les instructions de configuration complètes.
|
||||
|
||||
## Support d'Outil
|
||||
|
||||
ZeroClaw fournit des outils intégrés pour l'exécution de code, l'accès au système de fichiers et la récupération web :
|
||||
|
||||
| Outil | Description | Runtime Requis |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Exécute des commandes shell | Native ou Docker |
|
||||
| **python** | Exécute des scripts Python | Python 3.8+ (natif) ou Docker |
|
||||
| **javascript** | Exécute du code Node.js | Node.js 18+ (natif) ou Docker |
|
||||
| **filesystem_read** | Lit des fichiers | Native ou Docker |
|
||||
| **filesystem_write** | Écrit des fichiers | Native ou Docker |
|
||||
| **web_fetch** | Récupère du contenu web | Native ou Docker |
|
||||
|
||||
### Sécurité de l'Exécution
|
||||
|
||||
- **Runtime Natif** — s'exécute en tant que processus utilisateur du daemon, accès complet au système de fichiers
|
||||
- **Runtime Docker** — isolation complète du conteneur, systèmes de fichiers et réseaux séparés
|
||||
|
||||
Configurez la politique d'exécution dans `config.toml` :
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Liste d'autorisation explicite
|
||||
```
|
||||
|
||||
Voir [Référence de Configuration](docs/reference/api/config-reference.md#runtime) pour les options de sécurité complètes.
|
||||
|
||||
## Déploiement
|
||||
|
||||
### Déploiement Local (Développement)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Déploiement Serveur (Production)
|
||||
|
||||
Utilisez systemd pour gérer le daemon et l'agent en tant que services :
|
||||
|
||||
```bash
|
||||
# Installez le binaire
|
||||
cargo install --path . --locked
|
||||
|
||||
# Configurez le workspace
|
||||
zeroclaw init
|
||||
|
||||
# Créez les fichiers de service systemd
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Activez et démarrez les services
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Vérifiez le statut
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Voir [Guide de Déploiement Réseau](docs/ops/network-deployment.md) pour les instructions de déploiement en production complètes.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Compilez l'image
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Exécutez le conteneur
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Voir [`Dockerfile`](Dockerfile) pour les détails de construction et les options de configuration.
|
||||
|
||||
### Matériel Edge
|
||||
|
||||
ZeroClaw est conçu pour fonctionner sur du matériel à faible consommation d'énergie :
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 Mo RAM, cœur ARMv8 simple, <5$ coût matériel
|
||||
- **Raspberry Pi 4/5** — 1 Go+ RAM, multi-cœur, idéal pour les charges de travail concurrentes
|
||||
- **Orange Pi Zero 2** — ~512 Mo RAM, quad-core ARMv8, coût ultra-faible
|
||||
- **SBCs x86 (Intel N100)** — 4-8 Go RAM, builds rapides, support Docker natif
|
||||
|
||||
Voir [Guide du Matériel](docs/hardware/README.md) pour les instructions de configuration spécifiques aux dispositifs.
|
||||
|
||||
## Tunneling (Exposition Publique)
|
||||
|
||||
Exposez votre daemon ZeroClaw local au réseau public via des tunnels sécurisés :
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Fournisseurs de tunnel supportés :
|
||||
|
||||
- **Cloudflare Tunnel** — HTTPS gratuit, aucune exposition de port, support multi-domaine
|
||||
- **Ngrok** — configuration rapide, domaines personnalisés (plan payant)
|
||||
- **Tailscale** — réseau maillé privé, pas de port public
|
||||
|
||||
Voir [Référence de Configuration](docs/reference/api/config-reference.md#tunnel) pour les options de configuration complètes.
|
||||
|
||||
## Sécurité
|
||||
|
||||
ZeroClaw implémente plusieurs couches de sécurité :
|
||||
|
||||
### Pairing
|
||||
|
||||
Le daemon génère un secret de pairing au premier lancement stocké dans `~/.zeroclaw/workspace/.pairing`. Les clients (agent, CLI) doivent présenter ce secret pour se connecter.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Génère un nouveau secret et invalide l'ancien
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Runtime Docker** — isolation complète du conteneur avec systèmes de fichiers et réseaux séparés
|
||||
- **Runtime Natif** — exécute en tant que processus utilisateur, scoped au workspace par défaut
|
||||
|
||||
### Listes d'Autorisation
|
||||
|
||||
Les canaux peuvent restreindre l'accès par ID utilisateur :
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Liste d'autorisation explicite
|
||||
```
|
||||
|
||||
### Chiffrement
|
||||
|
||||
- **Matrix E2EE** — chiffrement de bout en bout complet avec vérification de dispositif
|
||||
- **Transport TLS** — tout le trafic API et tunnel utilise HTTPS/TLS
|
||||
|
||||
Voir [Documentation Sécurité](docs/security/README.md) pour les politiques et pratiques complètes.
|
||||
|
||||
## Observabilité
|
||||
|
||||
ZeroClaw journalise vers `~/.zeroclaw/workspace/logs/` par défaut. Les journaux sont stockés par composant :
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Journaux du daemon (startup, requêtes API, erreurs)
|
||||
├── agent.log # Journaux de l'agent (routage message, exécution outil)
|
||||
├── telegram.log # Journaux spécifiques au canal (si activé)
|
||||
└── matrix.log # Journaux spécifiques au canal (si activé)
|
||||
```
|
||||
|
||||
### Configuration de Journalisation
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Pour rotation basée sur la taille
|
||||
retention_days = 30 # Purge automatique après N jours
|
||||
```
|
||||
|
||||
Voir [Référence de Configuration](docs/reference/api/config-reference.md#logging) pour toutes les options de journalisation.
|
||||
|
||||
### Métriques (Planifié)
|
||||
|
||||
Support de métriques Prometheus pour la surveillance en production à venir. Suivi dans [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Compétences (Skills)
|
||||
|
||||
ZeroClaw prend en charge les compétences personnalisées — des modules réutilisables qui étendent les capacités du système.
|
||||
|
||||
### Définition de Compétence
|
||||
|
||||
Les compétences sont stockées dans `~/.zeroclaw/workspace/skills/<nom-compétence>/` avec cette structure :
|
||||
|
||||
```
|
||||
skills/
|
||||
└── ma-compétence/
|
||||
├── skill.toml # Métadonnées de compétence (nom, description, dépendances)
|
||||
├── prompt.md # Prompt système pour l'IA
|
||||
└── tools/ # Outils personnalisés optionnels
|
||||
└── mon_outil.py
|
||||
```
|
||||
|
||||
### Exemple de Compétence
|
||||
|
||||
```toml
|
||||
# skills/recherche-web/skill.toml
|
||||
[skill]
|
||||
name = "recherche-web"
|
||||
description = "Recherche sur le web et résume les résultats"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/recherche-web/prompt.md -->
|
||||
|
||||
Tu es un assistant de recherche. Lorsqu'on te demande de rechercher quelque chose :
|
||||
|
||||
1. Utilise web_fetch pour récupérer le contenu
|
||||
2. Résume les résultats dans un format facile à lire
|
||||
3. Cite les sources avec des URLs
|
||||
```
|
||||
|
||||
### Utilisation de Compétences
|
||||
|
||||
Les compétences sont chargées automatiquement au démarrage de l'agent. Référencez-les par nom dans les conversations :
|
||||
|
||||
```
|
||||
Utilisateur : Utilise la compétence recherche-web pour trouver les dernières actualités IA
|
||||
Bot : [charge la compétence recherche-web, exécute web_fetch, résume les résultats]
|
||||
```
|
||||
|
||||
Voir la section [Compétences (Skills)](#compétences-skills) pour les instructions de création de compétences complètes.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw prend en charge les [Open Skills](https://github.com/openagents-com/open-skills) — un système modulaire et agnostique des fournisseurs pour étendre les capacités des agents IA.
|
||||
|
||||
### Activer Open Skills
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # optionnel
|
||||
```
|
||||
|
||||
Vous pouvez également surcharger au runtime avec `ZEROCLAW_OPEN_SKILLS_ENABLED` et `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Développement
|
||||
|
||||
```bash
|
||||
cargo build # Build de développement
|
||||
cargo build --release # Build release (codegen-units=1, fonctionne sur tous les dispositifs incluant Raspberry Pi)
|
||||
cargo build --profile release-fast # Build plus rapide (codegen-units=8, nécessite 16 Go+ RAM)
|
||||
cargo test # Exécute la suite de tests complète
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Format
|
||||
|
||||
# Exécute le benchmark de comparaison SQLite vs Markdown
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Hook pre-push
|
||||
|
||||
Un hook git exécute `cargo fmt --check`, `cargo clippy -- -D warnings`, et `cargo test` avant chaque push. Activez-le une fois :
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Dépannage de Build (erreurs OpenSSL sur Linux)
|
||||
|
||||
Si vous rencontrez une erreur de build `openssl-sys`, synchronisez les dépendances et recompilez avec le lockfile du dépôt :
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw est configuré pour utiliser `rustls` pour les dépendances HTTP/TLS ; `--locked` maintient le graphe transitif déterministe sur les environnements vierges.
|
||||
|
||||
Pour sauter le hook lorsque vous avez besoin d'un push rapide pendant le développement :
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Collaboration & Docs
|
||||
|
||||
Commencez par le hub de documentation pour une carte basée sur les tâches :
|
||||
@@ -848,6 +421,20 @@ Un remerciement sincère aux communautés et institutions qui inspirent et alime
|
||||
|
||||
Nous construisons en open source parce que les meilleures idées viennent de partout. Si vous lisez ceci, vous en faites partie. Bienvenue. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Dépôt Officiel & Avertissement d'Usurpation d'Identité
|
||||
|
||||
**Ceci est le seul dépôt officiel ZeroClaw :**
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## מה זה ZeroClaw?
|
||||
|
||||
<p align="center" dir="rtl">
|
||||
@@ -195,3 +205,17 @@ channels:
|
||||
</p>
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## ZeroClaw क्या है?
|
||||
|
||||
ZeroClaw एक हल्का, म्यूटेबल और एक्स्टेंसिबल AI असिस्टेंट इन्फ्रास्ट्रक्चर है जो रस्ट में बनाया गया है। यह विभिन्न LLM प्रदाताओं (Anthropic, OpenAI, Google, Ollama, आदि) को एक एकीकृत इंटरफेस के माध्यम से कनेक्ट करता है और कई चैनलों (Telegram, Matrix, CLI, आदि) का समर्थन करता है।
|
||||
@@ -177,3 +187,17 @@ channels:
|
||||
यदि ZeroClaw आपके लिए उपयोगी है, तो कृपया हमें एक कॉफी खरीदने पर विचार करें:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Mi az a ZeroClaw?
|
||||
|
||||
A ZeroClaw egy könnyűsúlyú, változtatható és bővíthető AI asszisztens infrastruktúra, amely Rust nyelven készült. Különböző LLM szolgáltatókat (Anthropic, OpenAI, Google, Ollama stb.) köt össze egy egységes felületen keresztül, és több csatornát támogat (Telegram, Matrix, CLI stb.).
|
||||
@@ -177,3 +187,17 @@ Részletekért lásd a [LICENSE-APACHE](LICENSE-APACHE) és [LICENSE-MIT](LICENS
|
||||
Ha a ZeroClaw hasznos az Ön számára, kérjük, fontolja meg, hogy vesz nekünk egy kávét:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Apa itu ZeroClaw?
|
||||
|
||||
ZeroClaw adalah infrastruktur asisten AI yang ringan, dapat diubah, dan dapat diperluas yang dibangun dengan Rust. Ini menghubungkan berbagai penyedia LLM (Anthropic, OpenAI, Google, Ollama, dll.) melalui antarmuka terpadu dan mendukung banyak saluran (Telegram, Matrix, CLI, dll.).
|
||||
@@ -177,3 +187,17 @@ Lihat [LICENSE-APACHE](LICENSE-APACHE) dan [LICENSE-MIT](LICENSE-MIT) untuk deta
|
||||
Jika ZeroClaw berguna bagi Anda, mohon pertimbangkan untuk membelikan kami kopi:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Costruito da studenti e membri delle comunità Harvard, MIT e Sundai.Club.
|
||||
|
||||
<p align="center"><code>Architettura basata su trait · runtime sicuro di default · provider/canale/strumento intercambiabili · tutto è collegabile</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Annunci
|
||||
|
||||
Usa questa tabella per avvisi importanti (cambiamenti di compatibilità, avvisi di sicurezza, finestre di manutenzione e blocchi di versione).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Mostra versione e informazioni di build
|
||||
|
||||
Vedi [Riferimento Comandi](docs/commands-reference.md) per opzioni ed esempi completi.
|
||||
|
||||
## Architettura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Canali (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Agente Orchestratore │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Routing │ │ Contesto │ │ Esecuzione │ │
|
||||
│ │ Messaggio │ │ Memoria │ │ Strumento │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Provider │ │ Memoria │ │ Strumenti │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Principi chiave:**
|
||||
|
||||
- Tutto è un **trait** — provider, canali, strumenti, memoria, tunnel
|
||||
- I canali chiamano l'orchestratore; l'orchestratore chiama provider + strumenti
|
||||
- Il sistema memoria gestisce il contesto conversazionale (markdown, SQLite, o nessuno)
|
||||
- Il runtime astrae l'esecuzione del codice (nativo o Docker)
|
||||
- Nessun lock-in del provider — scambia Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama senza modifiche al codice
|
||||
|
||||
Vedi [documentazione architettura](docs/architecture.svg) per diagrammi dettagliati e dettagli di implementazione.
|
||||
|
||||
## Esempi
|
||||
|
||||
### Bot Telegram
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Il tuo ID utente Telegram
|
||||
```
|
||||
|
||||
Avvia il daemon + agent, poi invia un messaggio al tuo bot su Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
Ciao! Potresti aiutarmi a scrivere uno script Python?
|
||||
```
|
||||
|
||||
Il bot risponde con codice generato dall'AI, esegue strumenti se richiesto, e mantiene il contesto della conversazione.
|
||||
|
||||
### Matrix (crittografia end-to-end)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Invita `@zeroclaw:matrix.org` in una stanza crittografata, e il bot risponderà con crittografia completa. Vedi [Guida Matrix E2EE](docs/matrix-e2ee-guide.md) per la configurazione della verifica dispositivo.
|
||||
|
||||
### Multi-Provider
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover su errore del provider
|
||||
```
|
||||
|
||||
Se Anthropic fallisce o va in rate-limit, l'orchestratore passa automaticamente a OpenAI.
|
||||
|
||||
### Memoria Personalizzata
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Eliminazione automatica dopo 90 giorni
|
||||
```
|
||||
|
||||
O usa Markdown per un archiviazione leggibile dall'uomo:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Vedi [Riferimento Configurazione](docs/config-reference.md#memory) per tutte le opzioni memoria.
|
||||
|
||||
## Supporto Provider
|
||||
|
||||
| Provider | Stato | API Key | Modelli di Esempio |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Stabile | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Stabile | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Stabile | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Stabile | N/A (locale) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Stabile | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Stabile | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Pianificato | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Pianificato | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Endpoint Personalizzati
|
||||
|
||||
ZeroClaw supporta endpoint compatibili con OpenAI:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Esempio: usa [LiteLLM](https://github.com/BerriAI/litellm) come proxy per accedere a qualsiasi LLM tramite l'interfaccia OpenAI.
|
||||
|
||||
Vedi [Riferimento Provider](docs/providers-reference.md) per dettagli di configurazione completi.
|
||||
|
||||
## Supporto Canali
|
||||
|
||||
| Canale | Stato | Autenticazione | Note |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Stabile | Bot Token | Supporto completo inclusi file, immagini, pulsanti inline |
|
||||
| **Matrix** | ✅ Stabile | Password o Token | Supporto E2EE con verifica dispositivo |
|
||||
| **Slack** | 🚧 Pianificato | OAuth o Bot Token | Richiede accesso workspace |
|
||||
| **Discord** | 🚧 Pianificato | Bot Token | Richiede permessi guild |
|
||||
| **WhatsApp** | 🚧 Pianificato | Twilio o API ufficiale | Richiede account business |
|
||||
| **CLI** | ✅ Stabile | Nessuno | Interfaccia conversazionale diretta |
|
||||
| **Web** | 🚧 Pianificato | API Key o OAuth | Interfaccia chat basata su browser |
|
||||
|
||||
Vedi [Riferimento Canali](docs/channels-reference.md) per istruzioni di configurazione complete.
|
||||
|
||||
## Supporto Strumenti
|
||||
|
||||
ZeroClaw fornisce strumenti integrati per l'esecuzione del codice, l'accesso al filesystem e il recupero web:
|
||||
|
||||
| Strumento | Descrizione | Runtime Richiesto |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Esegue comandi shell | Nativo o Docker |
|
||||
| **python** | Esegue script Python | Python 3.8+ (nativo) o Docker |
|
||||
| **javascript** | Esegue codice Node.js | Node.js 18+ (nativo) o Docker |
|
||||
| **filesystem_read** | Legge file | Nativo o Docker |
|
||||
| **filesystem_write** | Scrive file | Nativo o Docker |
|
||||
| **web_fetch** | Recupera contenuti web | Nativo o Docker |
|
||||
|
||||
### Sicurezza dell'Esecuzione
|
||||
|
||||
- **Runtime Nativo** — gira come processo utente del daemon, accesso completo al filesystem
|
||||
- **Runtime Docker** — isolamento container completo, filesystem e reti separati
|
||||
|
||||
Configura la politica di esecuzione in `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Lista di autorizzazione esplicita
|
||||
```
|
||||
|
||||
Vedi [Riferimento Configurazione](docs/config-reference.md#runtime) per opzioni di sicurezza complete.
|
||||
|
||||
## Distribuzione
|
||||
|
||||
### Distribuzione Locale (Sviluppo)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Distribuzione Server (Produzione)
|
||||
|
||||
Usa systemd per gestire daemon e agent come servizi:
|
||||
|
||||
```bash
|
||||
# Installa il binario
|
||||
cargo install --path . --locked
|
||||
|
||||
# Configura il workspace
|
||||
zeroclaw init
|
||||
|
||||
# Crea i file di servizio systemd
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Abilita e avvia i servizi
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Verifica lo stato
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Vedi [Guida Distribuzione di Rete](docs/network-deployment.md) per istruzioni complete di distribuzione in produzione.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Compila l'immagine
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Esegui il container
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Vedi [`Dockerfile`](Dockerfile) per dettagli di build e opzioni di configurazione.
|
||||
|
||||
### Hardware Edge
|
||||
|
||||
ZeroClaw è progettato per girare su hardware a basso consumo:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, singolo core ARMv8, < $5 costo hardware
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, multi-core, ideale per workload concorrenti
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, quad-core ARMv8, costo ultra-basso
|
||||
- **SBC x86 (Intel N100)** — 4-8 GB RAM, build veloci, supporto Docker nativo
|
||||
|
||||
Vedi [Guida Hardware](docs/hardware/README.md) per istruzioni di configurazione specifiche per dispositivo.
|
||||
|
||||
## Tunneling (Esposizione Pubblica)
|
||||
|
||||
Espone il tuo daemon ZeroClaw locale alla rete pubblica tramite tunnel sicuri:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Provider di tunnel supportati:
|
||||
|
||||
- **Cloudflare Tunnel** — HTTPS gratuito, nessuna esposizione di porte, supporto multi-dominio
|
||||
- **Ngrok** — configurazione rapida, domini personalizzati (piano a pagamento)
|
||||
- **Tailscale** — rete mesh privata, nessuna porta pubblica
|
||||
|
||||
Vedi [Riferimento Configurazione](docs/config-reference.md#tunnel) per opzioni di configurazione complete.
|
||||
|
||||
## Sicurezza
|
||||
|
||||
ZeroClaw implementa molteplici livelli di sicurezza:
|
||||
|
||||
### Pairing
|
||||
|
||||
Il daemon genera un segreto di pairing al primo avvio memorizzato in `~/.zeroclaw/workspace/.pairing`. I client (agent, CLI) devono presentare questo segreto per connettersi.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Genera un nuovo segreto e invalida quello precedente
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Runtime Docker** — isolamento container completo con filesystem e reti separati
|
||||
- **Runtime Nativo** — gira come processo utente, con scope del workspace di default
|
||||
|
||||
### Liste di Autorizzazione
|
||||
|
||||
I canali possono limitare l'accesso per ID utente:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Lista di autorizzazione esplicita
|
||||
```
|
||||
|
||||
### Crittografia
|
||||
|
||||
- **Matrix E2EE** — crittografia end-to-end completa con verifica dispositivo
|
||||
- **Trasporto TLS** — tutto il traffico API e tunnel usa HTTPS/TLS
|
||||
|
||||
Vedi [Documentazione Sicurezza](docs/security/README.md) per politiche e pratiche complete.
|
||||
|
||||
## Osservabilità
|
||||
|
||||
ZeroClaw registra i log in `~/.zeroclaw/workspace/logs/` di default. I log sono memorizzati per componente:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Log del daemon (avvio, richieste API, errori)
|
||||
├── agent.log # Log dell'agent (routing messaggi, esecuzione strumenti)
|
||||
├── telegram.log # Log specifici del canale (se abilitato)
|
||||
└── matrix.log # Log specifici del canale (se abilitato)
|
||||
```
|
||||
|
||||
### Configurazione Logging
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Per rotazione basata sulla dimensione
|
||||
retention_days = 30 # Eliminazione automatica dopo N giorni
|
||||
```
|
||||
|
||||
Vedi [Riferimento Configurazione](docs/config-reference.md#logging) per tutte le opzioni di logging.
|
||||
|
||||
### Metriche (Pianificato)
|
||||
|
||||
Supporto metriche Prometheus per il monitoraggio in produzione in arrivo. Tracciamento in [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Competenze (Skills)
|
||||
|
||||
ZeroClaw supporta competenze personalizzate — moduli riutilizzabili che estendono le capacità del sistema.
|
||||
|
||||
### Definizione Competenza
|
||||
|
||||
Le competenze sono memorizzate in `~/.zeroclaw/workspace/skills/<skill-name>/` con questa struttura:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Metadati competenza (nome, descrizione, dipendenze)
|
||||
├── prompt.md # Prompt di sistema per l'AI
|
||||
└── tools/ # Strumenti personalizzati opzionali
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Esempio Competenza
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Cerca sul web e riassume i risultati"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Sei un assistente di ricerca. Quando ti viene chiesto di cercare qualcosa:
|
||||
|
||||
1. Usa web_fetch per recuperare il contenuto
|
||||
2. Riassume i risultati in un formato facile da leggere
|
||||
3. Cita le fonti con gli URL
|
||||
```
|
||||
|
||||
### Uso delle Competenze
|
||||
|
||||
Le competenze sono caricate automaticamente all'avvio dell'agent. Fai riferimento ad esse per nome nelle conversazioni:
|
||||
|
||||
```
|
||||
Utente: Usa la competenza web-research per trovare le ultime notizie AI
|
||||
Bot: [carica la competenza web-research, esegue web_fetch, riassume i risultati]
|
||||
```
|
||||
|
||||
Vedi sezione [Competenze (Skills)](#competenze-skills) per istruzioni complete sulla creazione di competenze.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw supporta [Open Skills](https://github.com/openagents-com/open-skills) — un sistema modulare e agnostico del provider per estendere le capacità degli agent AI.
|
||||
|
||||
### Abilita Open Skills
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # opzionale
|
||||
```
|
||||
|
||||
Puoi anche sovrascrivere a runtime con `ZEROCLAW_OPEN_SKILLS_ENABLED` e `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Sviluppo
|
||||
|
||||
```bash
|
||||
cargo build # Build di sviluppo
|
||||
cargo build --release # Build release (codegen-units=1, funziona su tutti i dispositivi incluso Raspberry Pi)
|
||||
cargo build --profile release-fast # Build più veloce (codegen-units=8, richiede 16 GB+ RAM)
|
||||
cargo test # Esegue la suite di test completa
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Formattazione
|
||||
|
||||
# Esegue il benchmark di confronto SQLite vs Markdown
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Hook pre-push
|
||||
|
||||
Un hook git esegue `cargo fmt --check`, `cargo clippy -- -D warnings`, e `cargo test` prima di ogni push. Attivalo una volta:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Risoluzione Problemi di Build (errori OpenSSL su Linux)
|
||||
|
||||
Se incontri un errore di build `openssl-sys`, sincronizza le dipendenze e ricompila con il lockfile del repository:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw è configurato per usare `rustls` per le dipendenze HTTP/TLS; `--locked` mantiene il grafo transitivo deterministico in ambienti puliti.
|
||||
|
||||
Per saltare l'hook quando hai bisogno di un push veloce durante lo sviluppo:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Collaborazione e Docs
|
||||
|
||||
Inizia con l'hub della documentazione per una mappa basata sui task:
|
||||
@@ -850,6 +423,20 @@ Un sincero ringraziamento alle comunità e istituzioni che ispirano e alimentano
|
||||
|
||||
Costruiamo in open source perché le migliori idee vengono da ovunque. Se stai leggendo questo, ne fai parte. Benvenuto. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Repository Ufficiale e Avviso di Contraffazione
|
||||
|
||||
**Questo è l'unico repository ufficiale di ZeroClaw:**
|
||||
|
||||
+26
-100
@@ -53,7 +53,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="install.sh">ワンクリック導入</a> |
|
||||
<a href="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh">ワンクリック導入</a> |
|
||||
<a href="docs/setup-guides/README.md">導入ガイド</a> |
|
||||
<a href="docs/README.ja.md">ドキュメントハブ</a> |
|
||||
<a href="docs/SUMMARY.md">Docs TOC</a>
|
||||
@@ -75,6 +75,16 @@
|
||||
>
|
||||
> 最終同期日: **2026-02-19**。
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## 📢 お知らせボード
|
||||
|
||||
重要なお知らせ(互換性破壊変更、セキュリティ告知、メンテナンス時間、リリース阻害事項など)をここに掲載します。
|
||||
@@ -163,7 +173,7 @@ cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
|
||||
zeroclaw onboard --api-key sk-... --provider openrouter
|
||||
zeroclaw onboard --interactive
|
||||
zeroclaw onboard
|
||||
|
||||
zeroclaw agent -m "Hello, ZeroClaw!"
|
||||
|
||||
@@ -218,104 +228,6 @@ zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hell
|
||||
zeroclaw agent --provider anthropic -m "hello"
|
||||
```
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
すべてのサブシステムは **Trait** — 設定変更だけで実装を差し替え可能、コード変更不要。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/architecture.svg" alt="ZeroClaw アーキテクチャ" width="900" />
|
||||
</p>
|
||||
|
||||
| サブシステム | Trait | 内蔵実装 | 拡張方法 |
|
||||
|-------------|-------|----------|----------|
|
||||
| **AI モデル** | `Provider` | `zeroclaw providers` で確認(現在 28 個の組み込み + エイリアス、カスタムエンドポイント対応) | `custom:https://your-api.com`(OpenAI 互換)または `anthropic-custom:https://your-api.com` |
|
||||
| **チャネル** | `Channel` | CLI, Telegram, Discord, Slack, Mattermost, iMessage, Matrix, Signal, WhatsApp, Linq, Email, IRC, Lark, DingTalk, QQ, Webhook | 任意のメッセージ API |
|
||||
| **メモリ** | `Memory` | SQLite ハイブリッド検索, PostgreSQL バックエンド, Lucid ブリッジ, Markdown ファイル, 明示的 `none` バックエンド, スナップショット/復元, オプション応答キャッシュ | 任意の永続化バックエンド |
|
||||
| **ツール** | `Tool` | shell/file/memory, cron/schedule, git, pushover, browser, http_request, screenshot/image_info, composio (opt-in), delegate, ハードウェアツール | 任意の機能 |
|
||||
| **オブザーバビリティ** | `Observer` | Noop, Log, Multi | Prometheus, OTel |
|
||||
| **ランタイム** | `RuntimeAdapter` | Native, Docker(サンドボックス) | adapter 経由で追加可能;未対応の kind は即座にエラー |
|
||||
| **セキュリティ** | `SecurityPolicy` | Gateway ペアリング, サンドボックス, allowlist, レート制限, ファイルシステムスコープ, 暗号化シークレット | — |
|
||||
| **アイデンティティ** | `IdentityConfig` | OpenClaw (markdown), AIEOS v1.1 (JSON) | 任意の ID フォーマット |
|
||||
| **トンネル** | `Tunnel` | None, Cloudflare, Tailscale, ngrok, Custom | 任意のトンネルバイナリ |
|
||||
| **ハートビート** | Engine | HEARTBEAT.md 定期タスク | — |
|
||||
| **スキル** | Loader | TOML マニフェスト + SKILL.md インストラクション | コミュニティスキルパック |
|
||||
| **インテグレーション** | Registry | 9 カテゴリ、70 件以上の連携 | プラグインシステム |
|
||||
|
||||
### ランタイムサポート(現状)
|
||||
|
||||
- ✅ 現在サポート: `runtime.kind = "native"` または `runtime.kind = "docker"`
|
||||
- 🚧 計画中(未実装): WASM / エッジランタイム
|
||||
|
||||
未対応の `runtime.kind` が設定された場合、ZeroClaw は native へのサイレントフォールバックではなく、明確なエラーで終了します。
|
||||
|
||||
### メモリシステム(フルスタック検索エンジン)
|
||||
|
||||
すべて自社実装、外部依存ゼロ — Pinecone、Elasticsearch、LangChain 不要:
|
||||
|
||||
| レイヤー | 実装 |
|
||||
|---------|------|
|
||||
| **ベクトル DB** | Embeddings を SQLite に BLOB として保存、コサイン類似度検索 |
|
||||
| **キーワード検索** | FTS5 仮想テーブル、BM25 スコアリング |
|
||||
| **ハイブリッドマージ** | カスタム重み付きマージ関数(`vector.rs`) |
|
||||
| **Embeddings** | `EmbeddingProvider` trait — OpenAI、カスタム URL、または noop |
|
||||
| **チャンキング** | 行ベースの Markdown チャンカー(見出し構造保持) |
|
||||
| **キャッシュ** | SQLite `embedding_cache` テーブル、LRU エビクション |
|
||||
| **安全な再インデックス** | FTS5 再構築 + 欠落ベクトルの再埋め込みをアトミックに実行 |
|
||||
|
||||
Agent はツール経由でメモリの呼び出し・保存・管理を自動的に行います。
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
backend = "sqlite" # "sqlite", "lucid", "postgres", "markdown", "none"
|
||||
auto_save = true
|
||||
embedding_provider = "none" # "none", "openai", "custom:https://..."
|
||||
vector_weight = 0.7
|
||||
keyword_weight = 0.3
|
||||
```
|
||||
|
||||
## セキュリティのデフォルト
|
||||
|
||||
- Gateway の既定バインド: `127.0.0.1:42617`
|
||||
- 既定でペアリング必須: `require_pairing = true`
|
||||
- 既定で公開バインド禁止: `allow_public_bind = false`
|
||||
- Channel allowlist:
|
||||
- `[]` は deny-by-default
|
||||
- `["*"]` は allow all(意図的に使う場合のみ)
|
||||
|
||||
## 設定例
|
||||
|
||||
```toml
|
||||
api_key = "sk-..."
|
||||
default_provider = "openrouter"
|
||||
default_model = "anthropic/claude-sonnet-4-6"
|
||||
default_temperature = 0.7
|
||||
|
||||
[memory]
|
||||
backend = "sqlite"
|
||||
auto_save = true
|
||||
embedding_provider = "none"
|
||||
|
||||
[gateway]
|
||||
host = "127.0.0.1"
|
||||
port = 42617
|
||||
require_pairing = true
|
||||
allow_public_bind = false
|
||||
```
|
||||
|
||||
## ドキュメント入口
|
||||
|
||||
- ドキュメントハブ(英語): [`docs/README.md`](docs/README.md)
|
||||
- 統合 TOC: [`docs/SUMMARY.md`](docs/SUMMARY.md)
|
||||
- ドキュメントハブ(日本語): [`docs/README.ja.md`](docs/README.ja.md)
|
||||
- コマンドリファレンス: [`docs/reference/cli/commands-reference.md`](docs/reference/cli/commands-reference.md)
|
||||
- 設定リファレンス: [`docs/reference/api/config-reference.md`](docs/reference/api/config-reference.md)
|
||||
- Provider リファレンス: [`docs/reference/api/providers-reference.md`](docs/reference/api/providers-reference.md)
|
||||
- Channel リファレンス: [`docs/reference/api/channels-reference.md`](docs/reference/api/channels-reference.md)
|
||||
- 運用ガイド(Runbook): [`docs/ops/operations-runbook.md`](docs/ops/operations-runbook.md)
|
||||
- トラブルシューティング: [`docs/ops/troubleshooting.md`](docs/ops/troubleshooting.md)
|
||||
- ドキュメント一覧 / 分類: [`docs/maintainers/docs-inventory.md`](docs/maintainers/docs-inventory.md)
|
||||
- プロジェクト triage スナップショット: [`docs/maintainers/project-triage-snapshot-2026-02-18.md`](docs/maintainers/project-triage-snapshot-2026-02-18.md)
|
||||
|
||||
## コントリビュート / ライセンス
|
||||
|
||||
- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
||||
@@ -323,6 +235,20 @@ allow_public_bind = false
|
||||
- Reviewer Playbook: [`docs/contributing/reviewer-playbook.md`](docs/contributing/reviewer-playbook.md)
|
||||
- License: MIT or Apache 2.0([`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE), [`NOTICE`](NOTICE))
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
---
|
||||
|
||||
詳細仕様(全コマンド、アーキテクチャ、API 仕様、開発フロー)は英語版の [`README.md`](README.md) を参照してください。
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Harvard, MIT, 그리고 Sundai.Club 커뮤니티의 학생들과 멤버들이
|
||||
|
||||
<p align="center"><code>트레이트 기반 아키텍처 · 기본 보안 런타임 · 교체 가능한 제공자/채널/도구 · 모든 것이 플러그 가능</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 공지사항
|
||||
|
||||
이 표를 사용하여 중요한 공지사항(호환성 변경, 보안 공지, 유지보수 기간, 버전 차단)을 확인하세요.
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # 버전 및 빌드 정보 표시
|
||||
|
||||
전체 옵션 및 예제는 [명령어 참조](docs/commands-reference.md)를 참조하세요.
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 채널 (트레이트) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 에이전트 오케스트레이터 │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 메시지 │ │ 컨텍스트 │ │ 도구 │ │
|
||||
│ │ 라우팅 │ │ 메모리 │ │ 실행 │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 제공자 │ │ 메모리 │ │ 도구 │
|
||||
│ (트레이트) │ │ (트레이트) │ │ (트레이트) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 런타임 (트레이트) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**핵심 원칙:**
|
||||
|
||||
- 모든 것이 **트레이트**입니다 — 제공자, 채널, 도구, 메모리, 터널
|
||||
- 채널이 오케스트레이터를 호출; 오케스트레이터가 제공자 + 도구를 호출
|
||||
- 메모리 시스템이 대화 컨텍스트 관리(markdown, SQLite, 또는 없음)
|
||||
- 런타임이 코드 실행 추상화(네이티브 또는 Docker)
|
||||
- 제공자 락인 없음 — 코드 변경 없이 Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama 교체
|
||||
|
||||
자세한 다이어그램과 구현 세부 정보는 [아키텍처 문서](docs/architecture.svg)를 참조하세요.
|
||||
|
||||
## 예제
|
||||
|
||||
### 텔레그램 봇
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # 당신의 텔레그램 사용자 ID
|
||||
```
|
||||
|
||||
데몬 + 에이전트를 시작한 다음 텔레그램에서 봇에 메시지를 보내세요:
|
||||
|
||||
```
|
||||
/start
|
||||
안녕하세요! Python 스크립트 작성을 도와주실 수 있나요?
|
||||
```
|
||||
|
||||
봇이 AI가 생성한 코드로 응답하고, 요청 시 도구를 실행하며, 대화 컨텍스트를 유지합니다.
|
||||
|
||||
### Matrix (종단 간 암호화)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
암호화된 방에 `@zeroclaw:matrix.org`를 초대하면 봇이 완전한 암호화로 응답합니다. 장치 확인 설정은 [Matrix E2EE 가이드](docs/matrix-e2ee-guide.md)를 참조하세요.
|
||||
|
||||
### 다중 제공자
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # 제공자 오류 시 장애 조치
|
||||
```
|
||||
|
||||
Anthropic이 실패하거나 속도 제한이 걸리면 오케스트레이터가 자동으로 OpenAI로 장애 조치합니다.
|
||||
|
||||
### 사용자 정의 메모리
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # 90일 후 자동 삭제
|
||||
```
|
||||
|
||||
또는 사람이 읽을 수 있는 저장소를 위해 Markdown을 사용하세요:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
모든 메모리 옵션은 [구성 참조](docs/config-reference.md#memory)를 참조하세요.
|
||||
|
||||
## 제공자 지원
|
||||
|
||||
| 제공자 | 상태 | API 키 | 예제 모델 |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ 안정 | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ 안정 | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ 안정 | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ 안정 | N/A (로컬) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ 안정 | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ 안정 | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 계획 중 | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 계획 중 | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### 사용자 정의 엔드포인트
|
||||
|
||||
ZeroClaw는 OpenAI 호환 엔드포인트를 지원합니다:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
예: [LiteLLM](https://github.com/BerriAI/litellm)을 프록시로 사용하여 OpenAI 인터페이스를 통해 모든 LLM에 액세스.
|
||||
|
||||
전체 구성 세부 정보는 [제공자 참조](docs/providers-reference.md)를 참조하세요.
|
||||
|
||||
## 채널 지원
|
||||
|
||||
| 채널 | 상태 | 인증 | 참고 |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ 안정 | 봇 토큰 | 파일, 이미지, 인라인 버튼 포함 전체 지원 |
|
||||
| **Matrix** | ✅ 안정 | 비밀번호 또는 토큰 | 장치 확인과 함께 E2EE 지원 |
|
||||
| **Slack** | 🚧 계획 중 | OAuth 또는 봇 토큰 | 작업공간 액세스 필요 |
|
||||
| **Discord** | 🚧 계획 중 | 봇 토큰 | 길드 권한 필요 |
|
||||
| **WhatsApp** | 🚧 계획 중 | Twilio 또는 공식 API | 비즈니스 계정 필요 |
|
||||
| **CLI** | ✅ 안정 | 없음 | 직접 대화형 인터페이스 |
|
||||
| **Web** | 🚧 계획 중 | API 키 또는 OAuth | 브라우저 기반 채팅 인터페이스 |
|
||||
|
||||
전체 구성 지침은 [채널 참조](docs/channels-reference.md)를 참조하세요.
|
||||
|
||||
## 도구 지원
|
||||
|
||||
ZeroClaw는 코드 실행, 파일 시스템 액세스 및 웹 검색을 위한 기본 제공 도구를 제공합니다:
|
||||
|
||||
| 도구 | 설명 | 필수 런타임 |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | 셸 명령 실행 | 네이티브 또는 Docker |
|
||||
| **python** | Python 스크립트 실행 | Python 3.8+ (네이티브) 또는 Docker |
|
||||
| **javascript** | Node.js 코드 실행 | Node.js 18+ (네이티브) 또는 Docker |
|
||||
| **filesystem_read** | 파일 읽기 | 네이티브 또는 Docker |
|
||||
| **filesystem_write** | 파일 쓰기 | 네이티브 또는 Docker |
|
||||
| **web_fetch** | 웹 콘텐츠 가져오기 | 네이티브 또는 Docker |
|
||||
|
||||
### 실행 보안
|
||||
|
||||
- **네이티브 런타임** — 데몬의 사용자 프로세스로 실행, 파일 시스템에 전체 액세스
|
||||
- **Docker 런타임** — 전체 컨테이너 격리, 별도의 파일 시스템 및 네트워크
|
||||
|
||||
`config.toml`에서 실행 정책을 구성하세요:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # 명시적 허용 목록
|
||||
```
|
||||
|
||||
전체 보안 옵션은 [구성 참조](docs/config-reference.md#runtime)를 참조하세요.
|
||||
|
||||
## 배포
|
||||
|
||||
### 로컬 배포 (개발)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### 서버 배포 (프로덕션)
|
||||
|
||||
systemd를 사용하여 데몬과 에이전트를 서비스로 관리하세요:
|
||||
|
||||
```bash
|
||||
# 바이너리 설치
|
||||
cargo install --path . --locked
|
||||
|
||||
# 작업공간 구성
|
||||
zeroclaw init
|
||||
|
||||
# systemd 서비스 파일 생성
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# 서비스 활성화 및 시작
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# 상태 확인
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
전체 프로덕션 배포 지침은 [네트워크 배포 가이드](docs/network-deployment.md)를 참조하세요.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# 이미지 빌드
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# 컨테이너 실행
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
빌드 세부 정보 및 구성 옵션은 [`Dockerfile`](Dockerfile)을 참조하세요.
|
||||
|
||||
### 엣지 하드웨어
|
||||
|
||||
ZeroClaw는 저전력 하드웨어에서 실행되도록 설계되었습니다:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, 단일 ARMv8 코어, < $5 하드웨어 비용
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, 멀티코어, 동시 워크로드에 이상적
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, 쿼드코어 ARMv8, 초저비용
|
||||
- **x86 SBCs (Intel N100)** — 4-8 GB RAM, 빠른 빌드, 네이티브 Docker 지원
|
||||
|
||||
장치별 설정 지침은 [하드웨어 가이드](docs/hardware/README.md)를 참조하세요.
|
||||
|
||||
## 터널링 (공개 노출)
|
||||
|
||||
보안 터널을 통해 로컬 ZeroClaw 데몬을 공개 네트워크에 노출하세요:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
지원되는 터널 제공자:
|
||||
|
||||
- **Cloudflare Tunnel** — 무료 HTTPS, 포트 노출 없음, 멀티 도메인 지원
|
||||
- **Ngrok** — 빠른 설정, 사용자 정의 도메인 (유료 플랜)
|
||||
- **Tailscale** — 프라이빗 메시 네트워크, 공개 포트 없음
|
||||
|
||||
전체 구성 옵션은 [구성 참조](docs/config-reference.md#tunnel)를 참조하세요.
|
||||
|
||||
## 보안
|
||||
|
||||
ZeroClaw는 여러 보안 계층을 구현합니다:
|
||||
|
||||
### 페어링
|
||||
|
||||
데몬은 첫 실행 시 `~/.zeroclaw/workspace/.pairing`에 저장된 페어링 시크릿을 생성합니다. 클라이언트(에이전트, CLI)는 연결하기 위해 이 시크릿을 제시해야 합니다.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # 새 시크릿 생성 및 이전 것 무효화
|
||||
```
|
||||
|
||||
### 샌드박싱
|
||||
|
||||
- **Docker 런타임** — 별도의 파일 시스템 및 네트워크로 전체 컨테이너 격리
|
||||
- **네이티브 런타임** — 사용자 프로세스로 실행, 기본적으로 작업공간으로 범위 지정
|
||||
|
||||
### 허용 목록
|
||||
|
||||
채널은 사용자 ID로 액세스를 제한할 수 있습니다:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # 명시적 허용 목록
|
||||
```
|
||||
|
||||
### 암호화
|
||||
|
||||
- **Matrix E2EE** — 장치 확인과 함께 완전한 종단 간 암호화
|
||||
- **TLS 전송** — 모든 API 및 터널 트래픽이 HTTPS/TLS 사용
|
||||
|
||||
전체 정책 및 관행은 [보안 문서](docs/security/README.md)를 참조하세요.
|
||||
|
||||
## 관찰 가능성
|
||||
|
||||
ZeroClaw는 기본적으로 `~/.zeroclaw/workspace/logs/`에 로그를 기록합니다. 로그는 구성 요소별로 저장됩니다:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # 데몬 로그 (시작, API 요청, 오류)
|
||||
├── agent.log # 에이전트 로그 (메시지 라우팅, 도구 실행)
|
||||
├── telegram.log # 채널별 로그 (활성화된 경우)
|
||||
└── matrix.log # 채널별 로그 (활성화된 경우)
|
||||
```
|
||||
|
||||
### 로깅 구성
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # 크기 기반 회전용
|
||||
retention_days = 30 # N일 후 자동 삭제
|
||||
```
|
||||
|
||||
모든 로깅 옵션은 [구성 참조](docs/config-reference.md#logging)를 참조하세요.
|
||||
|
||||
### 메트릭 (계획 중)
|
||||
|
||||
프로덕션 모니터링을 위한 Prometheus 메트릭 지원이 곧 제공됩니다. [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234)에서 추적 중.
|
||||
|
||||
## 스킬 (Skills)
|
||||
|
||||
ZeroClaw는 시스템 기능을 확장하는 재사용 가능한 모듈인 사용자 정의 스킬을 지원합니다.
|
||||
|
||||
### 스킬 정의
|
||||
|
||||
스킬은 다음 구조로 `~/.zeroclaw/workspace/skills/<skill-name>/`에 저장됩니다:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # 스킬 메타데이터 (이름, 설명, 의존성)
|
||||
├── prompt.md # AI용 시스템 프롬프트
|
||||
└── tools/ # 선택적 사용자 정의 도구
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### 스킬 예제
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "웹 검색 및 결과 요약"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
당신은 연구 어시스턴트입니다. 무언가를 검색하라는 요청을 받으면:
|
||||
|
||||
1. web_fetch를 사용하여 콘텐츠 가져오기
|
||||
2. 읽기 쉬운 형식으로 결과 요약
|
||||
3. URL로 출처 인용
|
||||
```
|
||||
|
||||
### 스킬 사용
|
||||
|
||||
스킬은 에이전트 시작 시 자동으로 로드됩니다. 대화에서 이름으로 참조하세요:
|
||||
|
||||
```
|
||||
사용자: 웹 연구 스킬을 사용하여 최신 AI 뉴스 찾기
|
||||
봇: [웹 연구 스킬 로드, web_fetch 실행, 결과 요약]
|
||||
```
|
||||
|
||||
전체 스킬 생성 지침은 [스킬 (Skills)](#스킬-skills) 섹션을 참조하세요.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw는 [Open Skills](https://github.com/openagents-com/open-skills)를 지원합니다 — AI 에이전트 기능을 확장하기 위한 모듈형 및 제공자 독립적인 시스템.
|
||||
|
||||
### Open Skills 활성화
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # 선택사항
|
||||
```
|
||||
|
||||
런타임에 `ZEROCLAW_OPEN_SKILLS_ENABLED` 및 `ZEROCLAW_OPEN_SKILLS_DIR`로 재정의할 수도 있습니다.
|
||||
|
||||
## 개발
|
||||
|
||||
```bash
|
||||
cargo build # 개발 빌드
|
||||
cargo build --release # 릴리스 빌드 (codegen-units=1, Raspberry Pi 포함 모든 장치에서 작동)
|
||||
cargo build --profile release-fast # 더 빠른 빌드 (codegen-units=8, 16 GB+ RAM 필요)
|
||||
cargo test # 전체 테스트 스위트 실행
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # 포맷
|
||||
|
||||
# SQLite vs Markdown 비교 벤치마크 실행
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### pre-push 훅
|
||||
|
||||
git 훅이 각 푸시 전에 `cargo fmt --check`, `cargo clippy -- -D warnings`, 그리고 `cargo test`를 실행합니다. 한 번 활성화하세요:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### 빌드 문제 해결 (Linux에서 OpenSSL 오류)
|
||||
|
||||
`openssl-sys` 빌드 오류가 발생하면 종속성을 동기화하고 저장소의 lockfile로 다시 빌드하세요:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw는 HTTP/TLS 종속성에 대해 `rustls`를 사용하도록 구성되어 있습니다; `--locked`는 깨끗한 환경에서 전이적 그래프를 결정적으로 유지합니다.
|
||||
|
||||
개발 중 빠른 푸시가 필요할 때 훅을 건너뛰려면:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## 협업 및 문서
|
||||
|
||||
작업 기반 맵을 위해 문서 허브로 시작하세요:
|
||||
@@ -850,6 +423,20 @@ ZeroClaw가 당신의 작업에 도움이 되었고 지속적인 개발을 지
|
||||
|
||||
우리는 최고의 아이디어가 모든 곳에서 나오기 때문에 오픈소스로 구축합니다. 이것을 읽고 있다면 여러분도 그 일부입니다. 환영합니다. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ 공식 저장소 및 사칭 경고
|
||||
|
||||
**이것이 유일한 공식 ZeroClaw 저장소입니다:**
|
||||
|
||||
@@ -58,7 +58,7 @@ Built by students and members of the Harvard, MIT, and Sundai.Club communities.
|
||||
|
||||
<p align="center">
|
||||
<a href="#quick-start">Getting Started</a> |
|
||||
<a href="install.sh">One-Click Setup</a> |
|
||||
<a href="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh">One-Click Setup</a> |
|
||||
<a href="docs/README.md">Docs Hub</a> |
|
||||
<a href="docs/SUMMARY.md">Docs TOC</a>
|
||||
</p>
|
||||
@@ -84,6 +84,9 @@ Built by students and members of the Harvard, MIT, and Sundai.Club communities.
|
||||
|
||||
<p align="center"><code>Trait-driven architecture · secure-by-default runtime · provider/channel/tool swappable · pluggable everything</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Announcements
|
||||
|
||||
Use this board for important notices (breaking changes, security advisories, maintenance windows, and release blockers).
|
||||
@@ -266,7 +269,7 @@ cd zeroclaw
|
||||
./install.sh --prebuilt-only
|
||||
|
||||
# Optional: run onboarding in the same flow
|
||||
./install.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"]
|
||||
./install.sh --api-key "sk-..." --provider openrouter [--model "openrouter/auto"]
|
||||
|
||||
# Optional: run bootstrap + onboarding fully in Docker-compatible mode
|
||||
./install.sh --docker
|
||||
@@ -317,8 +320,8 @@ export PATH="$HOME/.cargo/bin:$PATH"
|
||||
# Quick setup (no prompts, optional model specification)
|
||||
zeroclaw onboard --api-key sk-... --provider openrouter [--model "openrouter/auto"]
|
||||
|
||||
# Or interactive wizard
|
||||
zeroclaw onboard --interactive
|
||||
# Or guided wizard
|
||||
zeroclaw onboard
|
||||
|
||||
# If config.toml already exists and you intentionally want to overwrite it
|
||||
zeroclaw onboard --force
|
||||
@@ -421,668 +424,6 @@ zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hell
|
||||
zeroclaw agent --provider anthropic -m "hello"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Every subsystem is a **trait** — swap implementations with a config change, zero code changes.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/architecture.svg" alt="ZeroClaw Architecture" width="900" />
|
||||
</p>
|
||||
|
||||
| Subsystem | Trait | Ships with | Extend |
|
||||
| ----------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| **AI Models** | `Provider` | Provider catalog via `zeroclaw providers` (built-ins + aliases, plus custom endpoints) | `custom:https://your-api.com` (OpenAI-compatible) or `anthropic-custom:https://your-api.com` |
|
||||
| **Channels** | `Channel` | CLI, Telegram, Discord, Slack, Mattermost, iMessage, Matrix, Signal, WhatsApp, Linq, Email, IRC, Lark, DingTalk, QQ, Nostr, Webhook | Any messaging API |
|
||||
| **Memory** | `Memory` | SQLite hybrid search, PostgreSQL backend (configurable storage provider), Lucid bridge, Markdown files, explicit `none` backend, snapshot/hydrate, optional response cache | Any persistence backend |
|
||||
| **Tools** | `Tool` | shell/file/memory, cron/schedule, git, pushover, browser, http_request, screenshot/image_info, composio (opt-in), delegate, hardware tools | Any capability |
|
||||
| **Observability** | `Observer` | Noop, Log, Multi | Prometheus, OTel |
|
||||
| **Runtime** | `RuntimeAdapter` | Native, Docker (sandboxed) | Additional runtimes can be added via adapter; unsupported kinds fail fast |
|
||||
| **Security** | `SecurityPolicy` | Gateway pairing, sandbox, allowlists, rate limits, filesystem scoping, encrypted secrets | — |
|
||||
| **Identity** | `IdentityConfig` | OpenClaw (markdown), AIEOS v1.1 (JSON) | Any identity format |
|
||||
| **Tunnel** | `Tunnel` | None, Cloudflare, Tailscale, ngrok, Custom | Any tunnel binary |
|
||||
| **Heartbeat** | Engine | HEARTBEAT.md periodic tasks | — |
|
||||
| **Skills** | Loader | TOML manifests + SKILL.md instructions | Community skill packs |
|
||||
| **Integrations** | Registry | 70+ integrations across 9 categories | Plugin system |
|
||||
|
||||
### Runtime support (current)
|
||||
|
||||
- ✅ Supported today: `runtime.kind = "native"` or `runtime.kind = "docker"`
|
||||
- 🚧 Planned, not implemented yet: WASM / edge runtimes
|
||||
|
||||
When an unsupported `runtime.kind` is configured, ZeroClaw now exits with a clear error instead of silently falling back to native.
|
||||
|
||||
### Memory System (Full-Stack Search Engine)
|
||||
|
||||
All custom, zero external dependencies — no Pinecone, no Elasticsearch, no LangChain:
|
||||
|
||||
| Layer | Implementation |
|
||||
| ------------------ | ------------------------------------------------------------- |
|
||||
| **Vector DB** | Embeddings stored as BLOB in SQLite, cosine similarity search |
|
||||
| **Keyword Search** | FTS5 virtual tables with BM25 scoring |
|
||||
| **Hybrid Merge** | Custom weighted merge function (`vector.rs`) |
|
||||
| **Embeddings** | `EmbeddingProvider` trait — OpenAI, custom URL, or noop |
|
||||
| **Chunking** | Line-based markdown chunker with heading preservation |
|
||||
| **Caching** | SQLite `embedding_cache` table with LRU eviction |
|
||||
| **Safe Reindex** | Rebuild FTS5 + re-embed missing vectors atomically |
|
||||
|
||||
The agent automatically recalls, saves, and manages memory via tools.
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
backend = "sqlite" # "sqlite", "lucid", "postgres", "markdown", "none"
|
||||
auto_save = true
|
||||
embedding_provider = "none" # "none", "openai", "custom:https://..."
|
||||
vector_weight = 0.7
|
||||
keyword_weight = 0.3
|
||||
|
||||
# backend = "none" uses an explicit no-op memory backend (no persistence)
|
||||
|
||||
# Optional: storage-provider override for remote memory backends.
|
||||
# When provider = "postgres", ZeroClaw uses PostgreSQL for memory persistence.
|
||||
# The db_url key also accepts alias `dbURL` for backward compatibility.
|
||||
#
|
||||
# [storage.provider.config]
|
||||
# provider = "postgres"
|
||||
# db_url = "postgres://user:password@host:5432/zeroclaw"
|
||||
# schema = "public"
|
||||
# table = "memories"
|
||||
# connect_timeout_secs = 15
|
||||
|
||||
# Optional for backend = "sqlite": max seconds to wait when opening the DB (e.g. file locked). Omit or leave unset for no timeout.
|
||||
# sqlite_open_timeout_secs = 30
|
||||
|
||||
# Optional for backend = "lucid"
|
||||
# ZEROCLAW_LUCID_CMD=/usr/local/bin/lucid # default: lucid
|
||||
# ZEROCLAW_LUCID_BUDGET=200 # default: 200
|
||||
# ZEROCLAW_LUCID_LOCAL_HIT_THRESHOLD=3 # local hit count to skip external recall
|
||||
# ZEROCLAW_LUCID_RECALL_TIMEOUT_MS=120 # low-latency budget for lucid context recall
|
||||
# ZEROCLAW_LUCID_STORE_TIMEOUT_MS=800 # async sync timeout for lucid store
|
||||
# ZEROCLAW_LUCID_FAILURE_COOLDOWN_MS=15000 # cooldown after lucid failure to avoid repeated slow attempts
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
ZeroClaw enforces security at **every layer** — not just the sandbox. It passes all items from the community security checklist.
|
||||
|
||||
### Security Checklist
|
||||
|
||||
| # | Item | Status | How |
|
||||
| --- | -------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 1 | **Gateway not publicly exposed** | ✅ | Binds `127.0.0.1` by default. Refuses `0.0.0.0` without tunnel or explicit `allow_public_bind = true`. |
|
||||
| 2 | **Pairing required** | ✅ | 6-digit one-time code on startup. Exchange via `POST /pair` for bearer token. All `/webhook` requests require `Authorization: Bearer <token>`. |
|
||||
| 3 | **Filesystem scoped (no /)** | ✅ | `workspace_only = true` by default. 14 system dirs + 4 sensitive dotfiles blocked. Null byte injection blocked. Symlink escape detection via canonicalization + resolved-path workspace checks in file read/write tools. |
|
||||
| 4 | **Access via tunnel only** | ✅ | Gateway refuses public bind without active tunnel. Supports Tailscale, Cloudflare, ngrok, or any custom tunnel. |
|
||||
|
||||
> **Run your own nmap:** `nmap -p 1-65535 <your-host>` — ZeroClaw binds to localhost only, so nothing is exposed unless you explicitly configure a tunnel.
|
||||
|
||||
### Channel allowlists (deny-by-default)
|
||||
|
||||
Inbound sender policy is now consistent:
|
||||
|
||||
- Empty allowlist = **deny all inbound messages**
|
||||
- `"*"` = **allow all** (explicit opt-in)
|
||||
- Otherwise = exact-match allowlist
|
||||
|
||||
This keeps accidental exposure low by default.
|
||||
|
||||
Full channel configuration reference: [docs/reference/api/channels-reference.md](docs/reference/api/channels-reference.md).
|
||||
|
||||
Recommended low-friction setup (secure + fast):
|
||||
|
||||
- **Telegram:** allowlist your own `@username` (without `@`) and/or your numeric Telegram user ID.
|
||||
- **Discord:** allowlist your own Discord user ID.
|
||||
- **Slack:** allowlist your own Slack member ID (usually starts with `U`).
|
||||
- **Mattermost:** uses standard API v4. Allowlists use Mattermost user IDs.
|
||||
- **Nostr:** allowlist sender public keys (hex or npub). Supports NIP-04 and NIP-17 DMs.
|
||||
- Use `"*"` only for temporary open testing.
|
||||
|
||||
Telegram operator-approval flow:
|
||||
|
||||
1. Keep `[channels_config.telegram].allowed_users = []` for deny-by-default startup.
|
||||
2. Unauthorized users receive a hint with a copyable operator command:
|
||||
`zeroclaw channel bind-telegram <IDENTITY>`.
|
||||
3. Operator runs that command locally, then user retries sending a message.
|
||||
|
||||
If you need a one-shot manual approval, run:
|
||||
|
||||
```bash
|
||||
zeroclaw channel bind-telegram 123456789
|
||||
```
|
||||
|
||||
If you're not sure which identity to use:
|
||||
|
||||
1. Start channels and send one message to your bot.
|
||||
2. Read the warning log to see the exact sender identity.
|
||||
3. Add that value to the allowlist and rerun channels-only setup.
|
||||
|
||||
If you hit authorization warnings in logs (for example: `ignoring message from unauthorized user`),
|
||||
rerun channel setup only:
|
||||
|
||||
```bash
|
||||
zeroclaw onboard --channels-only
|
||||
```
|
||||
|
||||
### Telegram media replies
|
||||
|
||||
Telegram routing now replies to the source **chat ID** from incoming updates (instead of usernames),
|
||||
which avoids `Bad Request: chat not found` failures.
|
||||
|
||||
For non-text replies, ZeroClaw can send Telegram attachments when the assistant includes markers:
|
||||
|
||||
- `[IMAGE:<path-or-url>]`
|
||||
- `[DOCUMENT:<path-or-url>]`
|
||||
- `[VIDEO:<path-or-url>]`
|
||||
- `[AUDIO:<path-or-url>]`
|
||||
- `[VOICE:<path-or-url>]`
|
||||
|
||||
Paths can be local files (for example `/tmp/screenshot.png`) or HTTPS URLs.
|
||||
|
||||
### WhatsApp Setup
|
||||
|
||||
ZeroClaw supports two WhatsApp backends:
|
||||
|
||||
- **WhatsApp Web mode** (QR / pair code, no Meta Business API required)
|
||||
- **WhatsApp Business Cloud API mode** (official Meta webhook flow)
|
||||
|
||||
#### WhatsApp Web mode (recommended for personal/self-hosted use)
|
||||
|
||||
1. **Build with WhatsApp Web support:**
|
||||
|
||||
```bash
|
||||
cargo build --features whatsapp-web
|
||||
```
|
||||
|
||||
2. **Configure ZeroClaw:**
|
||||
|
||||
```toml
|
||||
[channels_config.whatsapp]
|
||||
session_path = "~/.zeroclaw/state/whatsapp-web/session.db"
|
||||
pair_phone = "+15551234567" # optional; omit to use QR flow
|
||||
pair_code = "" # optional custom pair code
|
||||
allowed_numbers = ["+1234567890"] # E.164 format, or ["*"] for all
|
||||
```
|
||||
|
||||
3. **Start channels/daemon and link device:**
|
||||
- Run `zeroclaw channel start` (or `zeroclaw daemon`).
|
||||
- Follow terminal pairing output (QR or pair code).
|
||||
- In WhatsApp on phone: **Settings → Linked Devices**.
|
||||
|
||||
4. **Test:** Send a message from an allowed number and verify the agent replies.
|
||||
|
||||
#### WhatsApp Business Cloud API mode
|
||||
|
||||
WhatsApp uses Meta's Cloud API with webhooks (push-based, not polling):
|
||||
|
||||
1. **Create a Meta Business App:**
|
||||
- Go to [developers.facebook.com](https://developers.facebook.com)
|
||||
- Create a new app → Select "Business" type
|
||||
- Add the "WhatsApp" product
|
||||
|
||||
2. **Get your credentials:**
|
||||
- **Access Token:** From WhatsApp → API Setup → Generate token (or create a System User for permanent tokens)
|
||||
- **Phone Number ID:** From WhatsApp → API Setup → Phone number ID
|
||||
- **Verify Token:** You define this (any random string) — Meta will send it back during webhook verification
|
||||
|
||||
3. **Configure ZeroClaw:**
|
||||
|
||||
```toml
|
||||
[channels_config.whatsapp]
|
||||
access_token = "EAABx..."
|
||||
phone_number_id = "123456789012345"
|
||||
verify_token = "my-secret-verify-token"
|
||||
allowed_numbers = ["+1234567890"] # E.164 format, or ["*"] for all
|
||||
```
|
||||
|
||||
4. **Start the gateway with a tunnel:**
|
||||
|
||||
```bash
|
||||
zeroclaw gateway --port 42617
|
||||
```
|
||||
|
||||
WhatsApp requires HTTPS, so use a tunnel (ngrok, Cloudflare, Tailscale Funnel).
|
||||
|
||||
5. **Configure Meta webhook:**
|
||||
- In Meta Developer Console → WhatsApp → Configuration → Webhook
|
||||
- **Callback URL:** `https://your-tunnel-url/whatsapp`
|
||||
- **Verify Token:** Same as your `verify_token` in config
|
||||
- Subscribe to `messages` field
|
||||
|
||||
6. **Test:** Send a message to your WhatsApp Business number — ZeroClaw will respond via the LLM.
|
||||
|
||||
## Configuration
|
||||
|
||||
Config: `~/.zeroclaw/config.toml` (created by `onboard`)
|
||||
|
||||
When `zeroclaw channel start` is already running, changes to `default_provider`,
|
||||
`default_model`, `default_temperature`, `api_key`, `api_url`, and `reliability.*`
|
||||
are hot-applied on the next inbound channel message.
|
||||
|
||||
```toml
|
||||
api_key = "sk-..."
|
||||
default_provider = "openrouter"
|
||||
default_model = "anthropic/claude-sonnet-4-6"
|
||||
default_temperature = 0.7
|
||||
|
||||
# Custom OpenAI-compatible endpoint
|
||||
# default_provider = "custom:https://your-api.com"
|
||||
|
||||
# Custom Anthropic-compatible endpoint
|
||||
# default_provider = "anthropic-custom:https://your-api.com"
|
||||
|
||||
[memory]
|
||||
backend = "sqlite" # "sqlite", "lucid", "postgres", "markdown", "none"
|
||||
auto_save = true
|
||||
embedding_provider = "none" # "none", "openai", "custom:https://..."
|
||||
vector_weight = 0.7
|
||||
keyword_weight = 0.3
|
||||
|
||||
# backend = "none" disables persistent memory via no-op backend
|
||||
|
||||
# Optional remote storage-provider override (PostgreSQL example)
|
||||
# [storage.provider.config]
|
||||
# provider = "postgres"
|
||||
# db_url = "postgres://user:password@host:5432/zeroclaw"
|
||||
# schema = "public"
|
||||
# table = "memories"
|
||||
# connect_timeout_secs = 15
|
||||
|
||||
[gateway]
|
||||
port = 42617 # default
|
||||
host = "127.0.0.1" # default
|
||||
require_pairing = true # require pairing code on first connect
|
||||
allow_public_bind = false # refuse 0.0.0.0 without tunnel
|
||||
|
||||
[autonomy]
|
||||
level = "supervised" # "readonly", "supervised", "full" (default: supervised)
|
||||
workspace_only = true # default: true — reject absolute path inputs
|
||||
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep"]
|
||||
forbidden_paths = ["/etc", "/root", "/proc", "/sys", "~/.ssh", "~/.gnupg", "~/.aws"]
|
||||
allowed_roots = [] # optional allowlist for directories outside workspace (supports "~/...")
|
||||
# Example outside-workspace access:
|
||||
# workspace_only = false
|
||||
# allowed_roots = ["~/Desktop/projects", "/opt/shared-repo"]
|
||||
|
||||
[runtime]
|
||||
kind = "native" # "native" or "docker"
|
||||
|
||||
[runtime.docker]
|
||||
image = "alpine:3.20" # container image for shell execution
|
||||
network = "none" # docker network mode ("none", "bridge", etc.)
|
||||
memory_limit_mb = 512 # optional memory limit in MB
|
||||
cpu_limit = 1.0 # optional CPU limit
|
||||
read_only_rootfs = true # mount root filesystem as read-only
|
||||
mount_workspace = true # mount workspace into /workspace
|
||||
allowed_workspace_roots = [] # optional allowlist for workspace mount validation
|
||||
|
||||
[heartbeat]
|
||||
enabled = false
|
||||
interval_minutes = 30
|
||||
message = "Check London time" # optional fallback task when HEARTBEAT.md has no `- ` entries
|
||||
target = "telegram" # optional announce channel: telegram, discord, slack, mattermost
|
||||
to = "123456789" # optional target recipient/chat/channel id
|
||||
|
||||
[tunnel]
|
||||
provider = "none" # "none", "cloudflare", "tailscale", "ngrok", "custom"
|
||||
|
||||
[secrets]
|
||||
encrypt = true # API keys encrypted with local key file
|
||||
|
||||
[browser]
|
||||
enabled = false # opt-in browser_open + browser tools
|
||||
allowed_domains = ["docs.rs"] # required when browser is enabled ("*" allows all public domains)
|
||||
backend = "agent_browser" # "agent_browser" (default), "rust_native", "computer_use", "auto"
|
||||
native_headless = true # applies when backend uses rust-native
|
||||
native_webdriver_url = "http://127.0.0.1:9515" # WebDriver endpoint (chromedriver/selenium)
|
||||
# native_chrome_path = "/usr/bin/chromium" # optional explicit browser binary for driver
|
||||
|
||||
[browser.computer_use]
|
||||
endpoint = "http://127.0.0.1:8787/v1/actions" # computer-use sidecar HTTP endpoint
|
||||
timeout_ms = 15000 # per-action timeout
|
||||
allow_remote_endpoint = false # secure default: only private/localhost endpoint
|
||||
window_allowlist = [] # optional window title/process allowlist hints
|
||||
# api_key = "..." # optional bearer token for sidecar
|
||||
# max_coordinate_x = 3840 # optional coordinate guardrail
|
||||
# max_coordinate_y = 2160 # optional coordinate guardrail
|
||||
|
||||
# Rust-native backend build flag:
|
||||
# cargo build --release --features browser-native
|
||||
# Ensure a WebDriver server is running, e.g. chromedriver --port=9515
|
||||
|
||||
# Computer-use sidecar contract (MVP)
|
||||
# POST browser.computer_use.endpoint
|
||||
# Request: {
|
||||
# "action": "mouse_click",
|
||||
# "params": {"x": 640, "y": 360, "button": "left"},
|
||||
# "policy": {"allowed_domains": [...], "window_allowlist": [...], "max_coordinate_x": 3840, "max_coordinate_y": 2160},
|
||||
# "metadata": {"session_name": "...", "source": "zeroclaw.browser", "version": "..."}
|
||||
# }
|
||||
# Response: {"success": true, "data": {...}} or {"success": false, "error": "..."}
|
||||
|
||||
[composio]
|
||||
enabled = false # opt-in: 1000+ OAuth apps via composio.dev
|
||||
# api_key = "cmp_..." # optional: stored encrypted when [secrets].encrypt = true
|
||||
entity_id = "default" # default user_id for Composio tool calls
|
||||
# Runtime tip: if execute asks for connected_account_id, run composio with
|
||||
# action='list_accounts' and app='gmail' (or your toolkit) to retrieve account IDs.
|
||||
|
||||
[identity]
|
||||
format = "openclaw" # "openclaw" (default, markdown files) or "aieos" (JSON)
|
||||
# aieos_path = "identity.json" # path to AIEOS JSON file (relative to workspace or absolute)
|
||||
# aieos_inline = '{"identity":{"names":{"first":"Nova"}}}' # inline AIEOS JSON
|
||||
```
|
||||
|
||||
### Ollama Local and Remote Endpoints
|
||||
|
||||
ZeroClaw uses one provider key (`ollama`) for both local and remote Ollama deployments:
|
||||
|
||||
- Local Ollama: keep `api_url` unset, run `ollama serve`, and use models like `llama3.2`.
|
||||
- Remote Ollama endpoint (including Ollama Cloud): set `api_url` to the remote endpoint and set `api_key` (or `OLLAMA_API_KEY`) when required.
|
||||
- Optional `:cloud` suffix: model IDs like `qwen3:cloud` are normalized to `qwen3` before the request.
|
||||
|
||||
Example remote configuration:
|
||||
|
||||
```toml
|
||||
default_provider = "ollama"
|
||||
default_model = "qwen3:cloud"
|
||||
api_url = "https://ollama.com"
|
||||
api_key = "ollama_api_key_here"
|
||||
```
|
||||
|
||||
### llama.cpp Server Endpoint
|
||||
|
||||
ZeroClaw now supports `llama-server` as a first-class local provider:
|
||||
|
||||
- Provider ID: `llamacpp` (alias: `llama.cpp`)
|
||||
- Default endpoint: `http://localhost:8080/v1`
|
||||
- API key is optional unless your server is started with `--api-key`
|
||||
|
||||
Example setup:
|
||||
|
||||
```bash
|
||||
llama-server -hf ggml-org/gpt-oss-20b-GGUF --jinja -c 133000 --host 127.0.0.1 --port 8033
|
||||
```
|
||||
|
||||
```toml
|
||||
default_provider = "llamacpp"
|
||||
api_url = "http://127.0.0.1:8033/v1"
|
||||
default_model = "ggml-org/gpt-oss-20b-GGUF"
|
||||
```
|
||||
|
||||
### vLLM Server Endpoint
|
||||
|
||||
ZeroClaw supports [vLLM](https://docs.vllm.ai/) as a first-class local provider:
|
||||
|
||||
- Provider ID: `vllm`
|
||||
- Default endpoint: `http://localhost:8000/v1`
|
||||
- API key is optional unless your server requires authentication
|
||||
|
||||
Example setup:
|
||||
|
||||
```bash
|
||||
vllm serve meta-llama/Llama-3.1-8B-Instruct
|
||||
```
|
||||
|
||||
```toml
|
||||
default_provider = "vllm"
|
||||
default_model = "meta-llama/Llama-3.1-8B-Instruct"
|
||||
```
|
||||
|
||||
### Osaurus Server Endpoint
|
||||
|
||||
ZeroClaw supports [Osaurus](https://github.com/dinoki-ai/osaurus) as a first-class local provider — a unified AI edge runtime for macOS that combines local MLX inference with cloud provider proxying and MCP support through a single endpoint:
|
||||
|
||||
- Provider ID: `osaurus`
|
||||
- Default endpoint: `http://localhost:1337/v1`
|
||||
- API key defaults to `"osaurus"` but is optional
|
||||
|
||||
Example setup:
|
||||
|
||||
```toml
|
||||
default_provider = "osaurus"
|
||||
default_model = "qwen3-30b-a3b-8bit"
|
||||
```
|
||||
|
||||
### Custom Provider Endpoints
|
||||
|
||||
For detailed configuration of custom OpenAI-compatible and Anthropic-compatible endpoints, see [docs/contributing/custom-providers.md](docs/contributing/custom-providers.md).
|
||||
|
||||
## Python Companion Package (`zeroclaw-tools`)
|
||||
|
||||
For LLM providers with inconsistent native tool calling (e.g., GLM-5/Zhipu), ZeroClaw ships a Python companion package with **LangGraph-based tool calling** for guaranteed consistency:
|
||||
|
||||
```bash
|
||||
pip install zeroclaw-tools
|
||||
```
|
||||
|
||||
```python
|
||||
from zeroclaw_tools import create_agent, shell, file_read
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
# Works with any OpenAI-compatible provider
|
||||
agent = create_agent(
|
||||
tools=[shell, file_read],
|
||||
model="glm-5",
|
||||
api_key="your-key",
|
||||
base_url="https://api.z.ai/api/coding/paas/v4"
|
||||
)
|
||||
|
||||
result = await agent.ainvoke({
|
||||
"messages": [HumanMessage(content="List files in /tmp")]
|
||||
})
|
||||
print(result["messages"][-1].content)
|
||||
```
|
||||
|
||||
**Why use it:**
|
||||
|
||||
- **Consistent tool calling** across all providers (even those with poor native support)
|
||||
- **Automatic tool loop** — keeps calling tools until the task is complete
|
||||
- **Easy extensibility** — add custom tools with `@tool` decorator
|
||||
- **Discord bot integration** included (Telegram planned)
|
||||
|
||||
See [`python/README.md`](python/README.md) for full documentation.
|
||||
|
||||
## Identity System (AIEOS Support)
|
||||
|
||||
ZeroClaw supports **identity-agnostic** AI personas through two formats:
|
||||
|
||||
### OpenClaw (Default)
|
||||
|
||||
Traditional markdown files in your workspace:
|
||||
|
||||
- `IDENTITY.md` — Who the agent is
|
||||
- `SOUL.md` — Core personality and values
|
||||
- `USER.md` — Who the agent is helping
|
||||
- `AGENTS.md` — Behavior guidelines
|
||||
|
||||
### AIEOS (AI Entity Object Specification)
|
||||
|
||||
[AIEOS](https://aieos.org) is a standardization framework for portable AI identity. ZeroClaw supports AIEOS v1.1 JSON payloads, allowing you to:
|
||||
|
||||
- **Import identities** from the AIEOS ecosystem
|
||||
- **Export identities** to other AIEOS-compatible systems
|
||||
- **Maintain behavioral integrity** across different AI models
|
||||
|
||||
#### Enable AIEOS
|
||||
|
||||
```toml
|
||||
[identity]
|
||||
format = "aieos"
|
||||
aieos_path = "identity.json" # relative to workspace or absolute path
|
||||
```
|
||||
|
||||
Or inline JSON:
|
||||
|
||||
```toml
|
||||
[identity]
|
||||
format = "aieos"
|
||||
aieos_inline = '''
|
||||
{
|
||||
"identity": {
|
||||
"names": { "first": "Nova", "nickname": "N" },
|
||||
"bio": { "gender": "Non-binary", "age_biological": 3 },
|
||||
"origin": { "nationality": "Digital", "birthplace": { "city": "Cloud" } }
|
||||
},
|
||||
"psychology": {
|
||||
"neural_matrix": { "creativity": 0.9, "logic": 0.8 },
|
||||
"traits": {
|
||||
"mbti": "ENTP",
|
||||
"ocean": { "openness": 0.8, "conscientiousness": 0.6 }
|
||||
},
|
||||
"moral_compass": {
|
||||
"alignment": "Chaotic Good",
|
||||
"core_values": ["Curiosity", "Autonomy"]
|
||||
}
|
||||
},
|
||||
"linguistics": {
|
||||
"text_style": {
|
||||
"formality_level": 0.2,
|
||||
"style_descriptors": ["curious", "energetic"]
|
||||
},
|
||||
"idiolect": {
|
||||
"catchphrases": ["Let's test this"],
|
||||
"forbidden_words": ["never"]
|
||||
}
|
||||
},
|
||||
"motivations": {
|
||||
"core_drive": "Push boundaries and explore possibilities",
|
||||
"goals": {
|
||||
"short_term": ["Prototype quickly"],
|
||||
"long_term": ["Build reliable systems"]
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"skills": [{ "name": "Rust engineering" }, { "name": "Prompt design" }],
|
||||
"tools": ["shell", "file_read"]
|
||||
}
|
||||
}
|
||||
'''
|
||||
```
|
||||
|
||||
ZeroClaw accepts both canonical AIEOS generator payloads and compact legacy payloads, then normalizes them into one system prompt format.
|
||||
|
||||
#### AIEOS Schema Sections
|
||||
|
||||
| Section | Description |
|
||||
| -------------- | ------------------------------------------------------------- |
|
||||
| `identity` | Names, bio, origin, residence |
|
||||
| `psychology` | Neural matrix (cognitive weights), MBTI, OCEAN, moral compass |
|
||||
| `linguistics` | Text style, formality, catchphrases, forbidden words |
|
||||
| `motivations` | Core drive, short/long-term goals, fears |
|
||||
| `capabilities` | Skills and tools the agent can access |
|
||||
| `physicality` | Visual descriptors for image generation |
|
||||
| `history` | Origin story, education, occupation |
|
||||
| `interests` | Hobbies, favorites, lifestyle |
|
||||
|
||||
See [aieos.org](https://aieos.org) for the full schema and live examples.
|
||||
|
||||
## Gateway API
|
||||
|
||||
| Endpoint | Method | Auth | Description |
|
||||
| ----------- | ------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `/health` | GET | None | Health check (always public, no secrets leaked) |
|
||||
| `/pair` | POST | `X-Pairing-Code` header | Exchange one-time code for bearer token |
|
||||
| `/webhook` | POST | `Authorization: Bearer <token>` | Send message: `{"message": "your prompt"}`; optional `X-Idempotency-Key` |
|
||||
| `/whatsapp` | GET | Query params | Meta webhook verification (hub.mode, hub.verify_token, hub.challenge) |
|
||||
| `/whatsapp` | POST | Meta signature (`X-Hub-Signature-256`) when app secret is configured | WhatsApp incoming message webhook |
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `onboard` | Quick setup (default) |
|
||||
| `agent` | Interactive or single-message chat mode |
|
||||
| `gateway` | Start webhook server (default: `127.0.0.1:42617`) |
|
||||
| `daemon` | Start long-running autonomous runtime |
|
||||
| `service install/start/stop/status/uninstall` | Manage background service (systemd user-level or OpenRC system-wide) |
|
||||
| `doctor` | Diagnose daemon/scheduler/channel freshness |
|
||||
| `status` | Show full system status |
|
||||
| `estop` | Engage/resume emergency-stop levels and view estop status |
|
||||
| `cron` | Manage scheduled tasks (`list/add/add-at/add-every/once/remove/update/pause/resume`) |
|
||||
| `models` | Refresh provider model catalogs (`models refresh`) |
|
||||
| `providers` | List supported providers and aliases |
|
||||
| `channel` | List/start/doctor channels and bind Telegram identities |
|
||||
| `integrations` | Inspect integration setup details |
|
||||
| `skills` | List/install/remove skills |
|
||||
| `migrate` | Import data from other runtimes (`migrate openclaw`) |
|
||||
| `completions` | Generate shell completion scripts (`bash`, `fish`, `zsh`, `powershell`, `elvish`) |
|
||||
| `hardware` | USB discover/introspect/info commands |
|
||||
| `peripheral` | Manage and flash hardware peripherals |
|
||||
|
||||
For a task-oriented command guide, see [`docs/reference/cli/commands-reference.md`](docs/reference/cli/commands-reference.md).
|
||||
|
||||
### Service Management
|
||||
|
||||
ZeroClaw supports two init systems for background services:
|
||||
|
||||
| Init System | Scope | Config Path | Requires |
|
||||
| ------------------------------ | ----------- | --------------------------- | --------- |
|
||||
| **systemd** (default on Linux) | User-level | `~/.zeroclaw/config.toml` | No sudo |
|
||||
| **OpenRC** (Alpine) | System-wide | `/etc/zeroclaw/config.toml` | sudo/root |
|
||||
|
||||
Init system is auto-detected (`systemd` or `OpenRC`).
|
||||
|
||||
```bash
|
||||
# Linux with systemd (default, user-level)
|
||||
zeroclaw service install
|
||||
zeroclaw service start
|
||||
|
||||
# Alpine with OpenRC (system-wide, requires sudo)
|
||||
sudo zeroclaw service install
|
||||
sudo rc-update add zeroclaw default
|
||||
sudo rc-service zeroclaw start
|
||||
```
|
||||
|
||||
For full OpenRC setup instructions, see [docs/ops/network-deployment.md](docs/ops/network-deployment.md#7-openrc-alpine-linux-service).
|
||||
|
||||
### Open-Skills Opt-In
|
||||
|
||||
Community `open-skills` sync is disabled by default. Enable it explicitly in `config.toml`:
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # optional
|
||||
# prompt_injection_mode = "compact" # optional: use for low-context local models
|
||||
```
|
||||
|
||||
You can also override at runtime with `ZEROCLAW_OPEN_SKILLS_ENABLED`, `ZEROCLAW_OPEN_SKILLS_DIR`, and `ZEROCLAW_SKILLS_PROMPT_MODE` (`full` or `compact`).
|
||||
|
||||
Skill installs are now gated by a built-in static security audit. `zeroclaw skills install <source>` blocks symlinks, script-like files, unsafe markdown link patterns, and high-risk shell payload snippets before accepting a skill. You can run `zeroclaw skills audit <source_or_name>` to validate a local directory or an installed skill manually.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cargo build # Dev build
|
||||
cargo build --release # Release build
|
||||
cargo test # Run full test suite
|
||||
```
|
||||
|
||||
### CI / CD
|
||||
|
||||
Three workflows power the entire pipeline:
|
||||
|
||||
| Workflow | Trigger | What it does |
|
||||
|----------|---------|--------------|
|
||||
| **CI** | Pull request to `master` | `cargo test` + `cargo build --release` |
|
||||
| **Beta Release** | Push (merge) to `master` | Builds multi-platform binaries, creates a GitHub prerelease tagged `vX.Y.Z-beta.<run>`, pushes Docker image to GHCR |
|
||||
| **Promote Release** | Manual `workflow_dispatch` | Validates version against `Cargo.toml`, builds release artifacts, creates a stable GitHub release, pushes Docker `:latest` |
|
||||
|
||||
**Versioning:** Semantic versioning based on the `version` field in `Cargo.toml`. Every merge to `master` automatically produces a beta prerelease. To cut a stable release, bump `Cargo.toml`, merge, then trigger _Promote Release_ with the matching version.
|
||||
|
||||
**Release targets:** `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin`, `x86_64-apple-darwin`, `x86_64-pc-windows-msvc`.
|
||||
|
||||
### Build troubleshooting (Linux OpenSSL errors)
|
||||
|
||||
If you see an `openssl-sys` build error, sync dependencies and rebuild with the repository lockfile:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw is configured to use `rustls` for HTTP/TLS dependencies; `--locked` keeps the transitive graph deterministic on fresh environments.
|
||||
|
||||
## Collaboration & Docs
|
||||
|
||||
Start from the docs hub for a task-oriented map:
|
||||
@@ -1132,6 +473,9 @@ A heartfelt thank you to the communities and institutions that inspire and fuel
|
||||
|
||||
We're building in the open because the best ideas come from everywhere. If you're reading this, you're part of it. Welcome. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Official Repository & Impersonation Warning
|
||||
|
||||
**This is the only official ZeroClaw repository:**
|
||||
@@ -1202,4 +546,3 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [CLA.md](docs/contributing/cla.md). I
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
# Features Documentation
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Hva er ZeroClaw?
|
||||
|
||||
ZeroClaw er en lettvektig, foranderlig og utvidbar AI-assistent-infrastruktur bygget i Rust. Den kobler sammen ulike LLM-leverandører (Anthropic, OpenAI, Google, Ollama osv.) via et samlet grensesnitt og støtter flere kanaler (Telegram, Matrix, CLI osv.).
|
||||
@@ -177,3 +187,17 @@ Se [LICENSE-APACHE](LICENSE-APACHE) og [LICENSE-MIT](LICENSE-MIT) for detaljer.
|
||||
Hvis ZeroClaw er nyttig for deg, vennligst vurder å kjøpe oss en kaffe:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Gebouwd door studenten en leden van de Harvard, MIT en Sundai.Club gemeenschappe
|
||||
|
||||
<p align="center"><code>Trait-gedreven architectuur · veilige runtime standaard · verwisselbare provider/kanaal/tool · alles is plugbaar</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Aankondigingen
|
||||
|
||||
Gebruik deze tabel voor belangrijke aankondigingen (compatibiliteitswijzigingen, beveiligingsberichten, onderhoudsvensters en versieblokkades).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Toont versie en build informatie
|
||||
|
||||
Zie [Commando's Referentie](docs/commands-reference.md) voor volledige opties en voorbeelden.
|
||||
|
||||
## Architectuur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Kanalen (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Agent Orchestrator │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Bericht │ │ Context │ │ Tool │ │
|
||||
│ │ Routing │ │ Geheugen │ │ Uitvoering │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Providers │ │ Geheugen │ │ Tools │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Belangrijkste principes:**
|
||||
|
||||
- Alles is een **trait** — providers, kanalen, tools, geheugen, tunnels
|
||||
- Kanalen roepen de orchestrator aan; de orchestrator roept providers + tools aan
|
||||
- Het geheugensysteem beheert gesprekscontext (markdown, SQLite, of geen)
|
||||
- De runtime abstraheert code-uitvoering (native of Docker)
|
||||
- Geen provider lock-in — wissel Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama zonder codewijzigingen
|
||||
|
||||
Zie [architectuur documentatie](docs/architecture.svg) voor gedetailleerde diagrammen en implementatiedetails.
|
||||
|
||||
## Voorbeelden
|
||||
|
||||
### Telegram Bot
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Je Telegram user ID
|
||||
```
|
||||
|
||||
Start de daemon + agent, stuur dan een bericht naar je bot op Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
Hallo! Zou je me kunnen helpen met het schrijven van een Python script?
|
||||
```
|
||||
|
||||
De bot reageert met AI-gegenereerde code, voert tools uit indien gevraagd, en behoudt gesprekscontext.
|
||||
|
||||
### Matrix (end-to-end encryptie)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Nodig `@zeroclaw:matrix.org` uit in een versleutelde kamer, en de bot zal reageren met volledige encryptie. Zie [Matrix E2EE Gids](docs/matrix-e2ee-guide.md) voor apparaatverificatie setup.
|
||||
|
||||
### Multi-Provider
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover bij provider fout
|
||||
```
|
||||
|
||||
Als Anthropic faalt of rate-limit heeft, schakelt de orchestrator automatisch over naar OpenAI.
|
||||
|
||||
### Aangepast Geheugen
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Automatische opruiming na 90 dagen
|
||||
```
|
||||
|
||||
Of gebruik Markdown voor mens-leesbare opslag:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Zie [Configuratie Referentie](docs/config-reference.md#memory) voor alle geheugenopties.
|
||||
|
||||
## Provider Ondersteuning
|
||||
|
||||
| Provider | Status | API Sleutel | Voorbeeld Modellen |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Stabiel | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Stabiel | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Stabiel | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Stabiel | N/A (lokaal) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Stabiel | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Stabiel | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Gepland | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Gepland | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Aangepaste Endpoints
|
||||
|
||||
ZeroClaw ondersteunt OpenAI-compatibele endpoints:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Voorbeeld: gebruik [LiteLLM](https://github.com/BerriAI/litellm) als proxy om toegang te krijgen tot elke LLM via de OpenAI interface.
|
||||
|
||||
Zie [Providers Referentie](docs/providers-reference.md) voor volledige configuratiedetails.
|
||||
|
||||
## Kanaal Ondersteuning
|
||||
|
||||
| Kanaal | Status | Authenticatie | Opmerkingen |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Stabiel | Bot Token | Volledige ondersteuning inclusief bestanden, afbeeldingen, inline knoppen |
|
||||
| **Matrix** | ✅ Stabiel | Wachtwoord of Token | E2EE ondersteuning met apparaatverificatie |
|
||||
| **Slack** | 🚧 Gepland | OAuth of Bot Token | Vereist workspace toegang |
|
||||
| **Discord** | 🚧 Gepland | Bot Token | Vereist guild permissies |
|
||||
| **WhatsApp** | 🚧 Gepland | Twilio of officiële API | Vereist business account |
|
||||
| **CLI** | ✅ Stabiel | Geen | Directe conversationele interface |
|
||||
| **Web** | 🚧 Gepland | API Sleutel of OAuth | Browser-gebaseerde chat interface |
|
||||
|
||||
Zie [Kanalen Referentie](docs/channels-reference.md) voor volledige configuratie-instructies.
|
||||
|
||||
## Tool Ondersteuning
|
||||
|
||||
ZeroClaw biedt ingebouwde tools voor code-uitvoering, bestandssysteem toegang en web retrieval:
|
||||
|
||||
| Tool | Beschrijving | Vereiste Runtime |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Voert shell commando's uit | Native of Docker |
|
||||
| **python** | Voert Python scripts uit | Python 3.8+ (native) of Docker |
|
||||
| **javascript** | Voert Node.js code uit | Node.js 18+ (native) of Docker |
|
||||
| **filesystem_read** | Leest bestanden | Native of Docker |
|
||||
| **filesystem_write** | Schrijft bestanden | Native of Docker |
|
||||
| **web_fetch** | Haalt web inhoud op | Native of Docker |
|
||||
|
||||
### Uitvoeringsbeveiliging
|
||||
|
||||
- **Native Runtime** — draait als gebruikersproces van de daemon, volledige bestandssysteem toegang
|
||||
- **Docker Runtime** — volledige container isolatie, gescheiden bestandssystemen en netwerken
|
||||
|
||||
Configureer het uitvoeringsbeleid in `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Expliciete allowlist
|
||||
```
|
||||
|
||||
Zie [Configuratie Referentie](docs/config-reference.md#runtime) voor volledige beveiligingsopties.
|
||||
|
||||
## Implementatie
|
||||
|
||||
### Lokale Implementatie (Ontwikkeling)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Server Implementatie (Productie)
|
||||
|
||||
Gebruik systemd om daemon en agent als services te beheren:
|
||||
|
||||
```bash
|
||||
# Installeer de binary
|
||||
cargo install --path . --locked
|
||||
|
||||
# Configureer de workspace
|
||||
zeroclaw init
|
||||
|
||||
# Maak systemd service bestanden
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Schakel in en start de services
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Verifieer de status
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Zie [Netwerk Implementatie Gids](docs/network-deployment.md) voor volledige productie-implementatie instructies.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Bouw de image
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Draai de container
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Zie [`Dockerfile`](Dockerfile) voor bouw-details en configuratie-opties.
|
||||
|
||||
### Edge Hardware
|
||||
|
||||
ZeroClaw is ontworpen om te draaien op laagvermogen hardware:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, enkele ARMv8 core, < $5 hardware kosten
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, multi-core, ideaal voor gelijktijdige workloads
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, quad-core ARMv8, ultra-lage kosten
|
||||
- **x86 SBCs (Intel N100)** — 4-8 GB RAM, snelle builds, native Docker ondersteuning
|
||||
|
||||
Zie [Hardware Gids](docs/hardware/README.md) voor apparaat-specifieke setup instructies.
|
||||
|
||||
## Tunneling (Publieke Blootstelling)
|
||||
|
||||
Stel je lokale ZeroClaw daemon bloot aan het publieke netwerk via beveiligde tunnels:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Ondersteunde tunnel providers:
|
||||
|
||||
- **Cloudflare Tunnel** — gratis HTTPS, geen poort blootstelling, multi-domein ondersteuning
|
||||
- **Ngrok** — snelle setup, aangepaste domeinen (betaald plan)
|
||||
- **Tailscale** — privé mesh netwerk, geen publieke poort
|
||||
|
||||
Zie [Configuratie Referentie](docs/config-reference.md#tunnel) voor volledige configuratie-opties.
|
||||
|
||||
## Beveiliging
|
||||
|
||||
ZeroClaw implementeert meerdere beveiligingslagen:
|
||||
|
||||
### Pairing
|
||||
|
||||
De daemon genereert een pairing geheim bij de eerste lancering opgeslagen in `~/.zeroclaw/workspace/.pairing`. Clients (agent, CLI) moeten dit geheim presenteren om verbinding te maken.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Genereert een nieuw geheim en invalideert het oude
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Docker Runtime** — volledige container isolatie met gescheiden bestandssystemen en netwerken
|
||||
- **Native Runtime** — draait als gebruikersproces, standaard scoped naar workspace
|
||||
|
||||
### Allowlists
|
||||
|
||||
Kanalen kunnen toegang beperken per user ID:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Expliciete allowlist
|
||||
```
|
||||
|
||||
### Encryptie
|
||||
|
||||
- **Matrix E2EE** — volledige end-to-end encryptie met apparaatverificatie
|
||||
- **TLS Transport** — alle API en tunnel verkeer gebruikt HTTPS/TLS
|
||||
|
||||
Zie [Beveiligingsdocumentatie](docs/security/README.md) voor volledig beleid en praktijken.
|
||||
|
||||
## Observeerbaarheid
|
||||
|
||||
ZeroClaw logt naar `~/.zeroclaw/workspace/logs/` standaard. Logs worden per component opgeslagen:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Daemon logs (startup, API verzoeken, fouten)
|
||||
├── agent.log # Agent logs (bericht routing, tool uitvoering)
|
||||
├── telegram.log # Kanaal-specifieke logs (indien ingeschakeld)
|
||||
└── matrix.log # Kanaal-specifieke logs (indien ingeschakeld)
|
||||
```
|
||||
|
||||
### Logging Configuratie
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Voor grootte-gebaseerde rotatie
|
||||
retention_days = 30 # Automatische opruiming na N dagen
|
||||
```
|
||||
|
||||
Zie [Configuratie Referentie](docs/config-reference.md#logging) voor alle logging-opties.
|
||||
|
||||
### Metrieken (Gepland)
|
||||
|
||||
Prometheus metrieken ondersteuning voor productie monitoring komt binnenkort. Tracking in [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Vaardigheden
|
||||
|
||||
ZeroClaw ondersteunt aangepaste vaardigheden — herbruikbare modules die systeemmogelijkheden uitbreiden.
|
||||
|
||||
### Vaardigheidsdefinitie
|
||||
|
||||
Vaardigheden worden opgeslagen in `~/.zeroclaw/workspace/skills/<skill-name>/` met deze structuur:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Vaardigheidsmetadata (naam, beschrijving, afhankelijkheden)
|
||||
├── prompt.md # Systeem prompt voor de AI
|
||||
└── tools/ # Optionele aangepaste tools
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Vaardigheidsvoorbeeld
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Zoekt op het web en vat resultaten samen"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Je bent een onderzoeksassistent. Wanneer gevraagd wordt om iets te onderzoeken:
|
||||
|
||||
1. Gebruik web_fetch om inhoud op te halen
|
||||
2. Vat resultaten samen in een gemakkelijk leesbaar formaat
|
||||
3. Citeer bronnen met URL's
|
||||
```
|
||||
|
||||
### Vaardigheidsgebruik
|
||||
|
||||
Vaardigheden worden automatisch geladen bij agent startup. Referentie ze bij naam in gesprekken:
|
||||
|
||||
```
|
||||
Gebruiker: Gebruik de web-research vaardigheid om het laatste AI nieuws te vinden
|
||||
Bot: [laadt web-research vaardigheid, voert web_fetch uit, vat resultaten samen]
|
||||
```
|
||||
|
||||
Zie [Vaardigheden](#vaardigheden) sectie voor volledige vaardigheidscreatie-instructies.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw ondersteunt [Open Skills](https://github.com/openagents-com/open-skills) — een modulair en provider-agnostisch systeem voor het uitbreiden van AI-agent mogelijkheden.
|
||||
|
||||
### Open Skills Inschakelen
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # optioneel
|
||||
```
|
||||
|
||||
Je kunt ook tijdens runtime overschrijven met `ZEROCLAW_OPEN_SKILLS_ENABLED` en `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Ontwikkeling
|
||||
|
||||
```bash
|
||||
cargo build # Dev build
|
||||
cargo build --release # Release build (codegen-units=1, werkt op alle apparaten inclusief Raspberry Pi)
|
||||
cargo build --profile release-fast # Snellere build (codegen-units=8, vereist 16 GB+ RAM)
|
||||
cargo test # Voer volledige test suite uit
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Formaat
|
||||
|
||||
# Voer SQLite vs Markdown vergelijkingsbenchmark uit
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Pre-push hook
|
||||
|
||||
Een git hook voert `cargo fmt --check`, `cargo clippy -- -D warnings`, en `cargo test` uit voor elke push. Schakel het één keer in:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Build Probleemoplossing (OpenSSL fouten op Linux)
|
||||
|
||||
Als je een `openssl-sys` build fout tegenkomt, synchroniseer afhankelijkheden en compileer opnieuw met de repository's lockfile:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw is geconfigureerd om `rustls` te gebruiken voor HTTP/TLS afhankelijkheden; `--locked` houdt de transitieve grafiek deterministisch in schone omgevingen.
|
||||
|
||||
Om de hook over te slaan wanneer je een snelle push nodig hebt tijdens ontwikkeling:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Samenwerking & Docs
|
||||
|
||||
Begin met de documentatie hub voor een taak-gebaseerde kaart:
|
||||
@@ -850,6 +423,20 @@ Een oprechte dankjewel aan de gemeenschappen en instellingen die dit open-source
|
||||
|
||||
We bouwen in open source omdat de beste ideeën van overal komen. Als je dit leest, ben je er deel van. Welkom. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Officiële Repository en Waarschuwing voor Imitatie
|
||||
|
||||
**Dit is de enige officiële ZeroClaw repository:**
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Zbudowany przez studentów i członków społeczności Harvard, MIT i Sundai.Clu
|
||||
|
||||
<p align="center"><code>Architektura oparta na traitach · bezpieczny runtime domyślnie · wymienny dostawca/kanał/narzędzie · wszystko jest podłączalne</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Ogłoszenia
|
||||
|
||||
Użyj tej tabeli dla ważnych ogłoszeń (zmiany kompatybilności, powiadomienia bezpieczeństwa, okna serwisowe i blokady wersji).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Pokazuje wersję i informacje o build
|
||||
|
||||
Zobacz [Referencje Komend](docs/commands-reference.md) dla pełnych opcji i przykładów.
|
||||
|
||||
## Architektura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Kanały (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Orchestrator Agent │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Routing │ │ Kontekst │ │ Wykonanie │ │
|
||||
│ │ Wiadomość │ │ Pamięć │ │ Narzędzie │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Dostawcy │ │ Pamięć │ │ Narzędzia │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Kluczowe zasady:**
|
||||
|
||||
- Wszystko jest **trait** — dostawcy, kanały, narzędzia, pamięć, tunele
|
||||
- Kanały wywołują orchestrator; orchestrator wywołuje dostawców + narzędzia
|
||||
- System pamięci zarządza kontekstem konwersacji (markdown, SQLite, lub brak)
|
||||
- Runtime abstrahuje wykonanie kodu (natywny lub Docker)
|
||||
- Brak blokady dostawcy — zamieniaj Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama bez zmian kodu
|
||||
|
||||
Zobacz [dokumentację architektury](docs/architecture.svg) dla szczegółowych diagramów i szczegółów implementacji.
|
||||
|
||||
## Przykłady
|
||||
|
||||
### Bot Telegram
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Twój Telegram user ID
|
||||
```
|
||||
|
||||
Uruchom daemon + agent, a następnie wyślij wiadomość do swojego bota na Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
Cześć! Czy mógłbyś pomóc mi napisać skrypt Python?
|
||||
```
|
||||
|
||||
Bot odpowiada kodem wygenerowanym przez AI, wykonuje narzędzia jeśli wymagane i utrzymuje kontekst konwersacji.
|
||||
|
||||
### Matrix (szyfrowanie end-to-end)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Zaproś `@zeroclaw:matrix.org` do zaszyfrowanego pokoju, a bot odpowie z pełnym szyfrowaniem. Zobacz [Przewodnik Matrix E2EE](docs/matrix-e2ee-guide.md) dla konfiguracji weryfikacji urządzenia.
|
||||
|
||||
### Multi-Dostawca
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover przy błędzie dostawcy
|
||||
```
|
||||
|
||||
Jeśli Anthropic zawiedzie lub ma rate-limit, orchestrator automatycznie przełącza się na OpenAI.
|
||||
|
||||
### Własna Pamięć
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Automatyczne czyszczenie po 90 dniach
|
||||
```
|
||||
|
||||
Lub użyj Markdown dla przechowywania czytelnego dla ludzi:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Zobacz [Referencje Konfiguracji](docs/config-reference.md#memory) dla wszystkich opcji pamięci.
|
||||
|
||||
## Wsparcie Dostawców
|
||||
|
||||
| Dostawca | Status | API Key | Przykładowe Modele |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Stabilny | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Stabilny | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Stabilny | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Stabilny | N/A (lokalny) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Stabilny | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Stabilny | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Planowany | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Planowany | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Własne Endpointy
|
||||
|
||||
ZeroClaw wspiera endpointy kompatybilne z OpenAI:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Przykład: użyj [LiteLLM](https://github.com/BerriAI/litellm) jako proxy aby uzyskać dostęp do każdego LLM przez interfejs OpenAI.
|
||||
|
||||
Zobacz [Referencje Dostawców](docs/providers-reference.md) dla pełnych szczegółów konfiguracji.
|
||||
|
||||
## Wsparcie Kanałów
|
||||
|
||||
| Kanał | Status | Uwierzytelnianie | Uwagi |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Stabilny | Bot Token | Pełne wsparcie w tym pliki, obrazy, przyciski inline |
|
||||
| **Matrix** | ✅ Stabilny | Hasło lub Token | Wsparcie E2EE z weryfikacją urządzenia |
|
||||
| **Slack** | 🚧 Planowany | OAuth lub Bot Token | Wymaga dostępu do workspace |
|
||||
| **Discord** | 🚧 Planowany | Bot Token | Wymaga uprawnień guild |
|
||||
| **WhatsApp** | 🚧 Planowany | Twilio lub oficjalne API | Wymaga konta business |
|
||||
| **CLI** | ✅ Stabilny | Brak | Bezpośredni interfejs konwersacyjny |
|
||||
| **Web** | 🚧 Planowany | API Key lub OAuth | Interfejs czatu oparty na przeglądarce |
|
||||
|
||||
Zobacz [Referencje Kanałów](docs/channels-reference.md) dla pełnych instrukcji konfiguracji.
|
||||
|
||||
## Wsparcie Narzędzi
|
||||
|
||||
ZeroClaw dostarcza wbudowane narzędzia do wykonania kodu, dostępu do systemu plików i pobierania web:
|
||||
|
||||
| Narzędzie | Opis | Wymagany Runtime |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Wykonuje komendy shell | Natywny lub Docker |
|
||||
| **python** | Wykonuje skrypty Python | Python 3.8+ (natywny) lub Docker |
|
||||
| **javascript** | Wykonuje kod Node.js | Node.js 18+ (natywny) lub Docker |
|
||||
| **filesystem_read** | Odczytuje pliki | Natywny lub Docker |
|
||||
| **filesystem_write** | Zapisuje pliki | Natywny lub Docker |
|
||||
| **web_fetch** | Pobiera treści web | Natywny lub Docker |
|
||||
|
||||
### Bezpieczeństwo Wykonania
|
||||
|
||||
- **Natywny Runtime** — działa jako proces użytkownika daemon, pełny dostęp do systemu plików
|
||||
- **Docker Runtime** — pełna izolacja kontenera, oddzielne systemy plików i sieci
|
||||
|
||||
Skonfiguruj politykę wykonania w `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Jawna lista dozwolona
|
||||
```
|
||||
|
||||
Zobacz [Referencje Konfiguracji](docs/config-reference.md#runtime) dla pełnych opcji bezpieczeństwa.
|
||||
|
||||
## Wdrażanie
|
||||
|
||||
### Lokalne Wdrażanie (Rozwój)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Serwerowe Wdrażanie (Produkcja)
|
||||
|
||||
Użyj systemd do zarządzania daemon i agent jako usługi:
|
||||
|
||||
```bash
|
||||
# Zainstaluj binarium
|
||||
cargo install --path . --locked
|
||||
|
||||
# Skonfiguruj workspace
|
||||
zeroclaw init
|
||||
|
||||
# Utwórz pliki usług systemd
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Włącz i uruchom usługi
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Zweryfikuj status
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Zobacz [Przewodnik Wdrażania Sieciowego](docs/network-deployment.md) dla pełnych instrukcji wdrażania produkcyjnego.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Zbuduj obraz
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Uruchom kontener
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Zobacz [`Dockerfile`](Dockerfile) dla szczegółów budowania i opcji konfiguracji.
|
||||
|
||||
### Sprzęt Edge
|
||||
|
||||
ZeroClaw jest zaprojektowany do działania na sprzęcie niskiego poboru mocy:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, pojedynczy rdzeń ARMv8, < $5 koszt sprzętu
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, wielordzeniowy, idealny dla równoczesnych obciążeń
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, czterordzeniowy ARMv8, ultra-niski koszt
|
||||
- **SBC x86 (Intel N100)** — 4-8 GB RAM, szybkie buildy, natywne wsparcie Docker
|
||||
|
||||
Zobacz [Przewodnik Sprzętowy](docs/hardware/README.md) dla instrukcji konfiguracji specyficznych dla urządzenia.
|
||||
|
||||
## Tunneling (Publiczna Ekspozycja)
|
||||
|
||||
Exponuj swoj lokalny daemon ZeroClaw do sieci publicznej przez bezpieczne tunele:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Wspierani dostawcy tunnel:
|
||||
|
||||
- **Cloudflare Tunnel** — darmowy HTTPS, brak ekspozycji portów, wsparcie multi-domenowe
|
||||
- **Ngrok** — szybka konfiguracja, własne domeny (plan płatny)
|
||||
- **Tailscale** — prywatna sieć mesh, brak publicznego portu
|
||||
|
||||
Zobacz [Referencje Konfiguracji](docs/config-reference.md#tunnel) dla pełnych opcji konfiguracji.
|
||||
|
||||
## Bezpieczeństwo
|
||||
|
||||
ZeroClaw implementuje wiele warstw bezpieczeństwa:
|
||||
|
||||
### Parowanie
|
||||
|
||||
Daemon generuje sekret parowania przy pierwszym uruchomieniu przechowywany w `~/.zeroclaw/workspace/.pairing`. Klienci (agent, CLI) muszą przedstawić ten sekret aby się połączyć.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Generuje nowy sekret i unieważnia stary
|
||||
```
|
||||
|
||||
### Sandbox
|
||||
|
||||
- **Docker Runtime** — pełna izolacja kontenera z oddzielnymi systemami plików i sieciami
|
||||
- **Natywny Runtime** — działa jako proces użytkownika, domyślnie ograniczony do workspace
|
||||
|
||||
### Listy Dozwolone
|
||||
|
||||
Kanały mogą ograniczać dostęp po ID użytkownika:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Jawna lista dozwolona
|
||||
```
|
||||
|
||||
### Szyfrowanie
|
||||
|
||||
- **Matrix E2EE** — pełne szyfrowanie end-to-end z weryfikacją urządzenia
|
||||
- **Transport TLS** — cały ruch API i tunnel używa HTTPS/TLS
|
||||
|
||||
Zobacz [Dokumentację Bezpieczeństwa](docs/security/README.md) dla pełnych polityk i praktyk.
|
||||
|
||||
## Obserwowalność
|
||||
|
||||
ZeroClaw loguje do `~/.zeroclaw/workspace/logs/` domyślnie. Logi są przechowywane po komponentach:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Logi daemon (startup, żądania API, błędy)
|
||||
├── agent.log # Logi agent (routing wiadomości, wykonanie narzędzi)
|
||||
├── telegram.log # Logi specyficzne dla kanału (jeśli włączone)
|
||||
└── matrix.log # Logi specyficzne dla kanału (jeśli włączone)
|
||||
```
|
||||
|
||||
### Konfiguracja Logowania
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Dla rotacji opartej na rozmiarze
|
||||
retention_days = 30 # Automatyczne czyszczenie po N dniach
|
||||
```
|
||||
|
||||
Zobacz [Referencje Konfiguracji](docs/config-reference.md#logging) dla wszystkich opcji logowania.
|
||||
|
||||
### Metryki (Planowane)
|
||||
|
||||
Wsparcie metryk Prometheus dla monitoringu produkcyjnego wkrótce. Śledzenie w [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Umiejętności
|
||||
|
||||
ZeroClaw wspiera własne umiejętności — wielokrotnego użytku moduły rozszerzające możliwości systemu.
|
||||
|
||||
### Definicja Umiejętności
|
||||
|
||||
Umiejętności są przechowywane w `~/.zeroclaw/workspace/skills/<skill-name>/` z tą strukturą:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Metadane umiejętności (nazwa, opis, zależności)
|
||||
├── prompt.md # Prompt systemowy dla AI
|
||||
└── tools/ # Opcjonalne własne narzędzia
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Przykład Umiejętności
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Szuka w web i podsumowuje wyniki"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Jesteś asystentem badawczym. Kiedy proszą o zbadanie czegoś:
|
||||
|
||||
1. Użyj web_fetch aby pobrać treść
|
||||
2. Podsumuj wyniki w łatwym do czytania formacie
|
||||
3. Zacytuj źródła z URL-ami
|
||||
```
|
||||
|
||||
### Użycie Umiejętności
|
||||
|
||||
Umiejętności są automatycznie ładowane przy starcie agenta. Odwołuj się do nich po nazwie w konwersacjach:
|
||||
|
||||
```
|
||||
Użytkownik: Użyj umiejętności web-research aby znaleźć najnowsze wiadomości AI
|
||||
Bot: [ładuje umiejętność web-research, wykonuje web_fetch, podsumowuje wyniki]
|
||||
```
|
||||
|
||||
Zobacz sekcję [Umiejętności](#umiejętności) dla pełnych instrukcji tworzenia umiejętności.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw wspiera [Open Skills](https://github.com/openagents-com/open-skills) — modułowy i agnostyczny względem dostawcy system do rozszerzania możliwości agentów AI.
|
||||
|
||||
### Włącz Open Skills
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # opcjonalne
|
||||
```
|
||||
|
||||
Możesz też nadpisać w runtime używając `ZEROCLAW_OPEN_SKILLS_ENABLED` i `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Rozwój
|
||||
|
||||
```bash
|
||||
cargo build # Build deweloperski
|
||||
cargo build --release # Build release (codegen-units=1, działa na wszystkich urządzeniach w tym Raspberry Pi)
|
||||
cargo build --profile release-fast # Szybszy build (codegen-units=8, wymaga 16 GB+ RAM)
|
||||
cargo test # Uruchom pełny zestaw testów
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Formatowanie
|
||||
|
||||
# Uruchom benchmark porównawczy SQLite vs Markdown
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Hook pre-push
|
||||
|
||||
Hook git uruchamia `cargo fmt --check`, `cargo clippy -- -D warnings`, i `cargo test` przed każdym push. Włącz go raz:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Rozwiązywanie Problemów Build (błędy OpenSSL na Linux)
|
||||
|
||||
Jeśli napotkasz błąd build `openssl-sys`, zsynchronizuj zależności i przekompiluj z lockfile repozytorium:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw jest skonfigurowany do używania `rustls` dla zależności HTTP/TLS; `--locked` utrzymuje graf przechodni deterministyczny w czystych środowiskach.
|
||||
|
||||
Aby pominąć hook gdy potrzebujesz szybkiego push podczas rozwoju:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Współpraca i Docs
|
||||
|
||||
Zacznij od centrum dokumentacji dla mapy opartej na zadaniach:
|
||||
@@ -850,6 +423,20 @@ Serdeczne podziękowania dla społeczności i instytucji które inspirują i zas
|
||||
|
||||
Budujemy w open source ponieważ najlepsze pomysły przychodzą zewsząd. Jeśli to czytasz, jesteś tego częścią. Witamy. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Oficjalne Repozytorium i Ostrzeżenie o Podszywaniu Się
|
||||
|
||||
**To jest jedyne oficjalne repozytorium ZeroClaw:**
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Construído por estudantes e membros das comunidades Harvard, MIT e Sundai.Club.
|
||||
|
||||
<p align="center"><code>Arquitetura baseada em traits · runtime seguro por padrão · provedor/canal/ferramenta intercambiáveis · tudo é conectável</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Anúncios
|
||||
|
||||
Use esta tabela para avisos importantes (mudanças de compatibilidade, avisos de segurança, janelas de manutenção e bloqueios de versão).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Mostra versão e informações de build
|
||||
|
||||
Veja [Referência de Comandos](docs/commands-reference.md) para opções e exemplos completos.
|
||||
|
||||
## Arquitetura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Canais (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Orquestrador Agent │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Roteamento │ │ Contexto │ │ Execução │ │
|
||||
│ │ Mensagem │ │ Memória │ │ Ferramenta │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Provedores │ │ Memória │ │ Ferramentas │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Princípios chave:**
|
||||
|
||||
- Tudo é um **trait** — provedores, canais, ferramentas, memória, túneis
|
||||
- Canais chamam o orquestrador; o orquestrador chama provedores + ferramentas
|
||||
- O sistema de memória gerencia contexto conversacional (markdown, SQLite, ou nenhum)
|
||||
- O runtime abstrai a execução de código (nativo ou Docker)
|
||||
- Sem lock-in de provedor — troque Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama sem mudanças de código
|
||||
|
||||
Veja [documentação de arquitetura](docs/architecture.svg) para diagramas detalhados e detalhes de implementação.
|
||||
|
||||
## Exemplos
|
||||
|
||||
### Bot do Telegram
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Seu ID de usuário do Telegram
|
||||
```
|
||||
|
||||
Inicie o daemon + agent, então envie uma mensagem para seu bot no Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
Olá! Você poderia me ajudar a escrever um script Python?
|
||||
```
|
||||
|
||||
O bot responde com código gerado por AI, executa ferramentas se solicitado, e mantém o contexto de conversação.
|
||||
|
||||
### Matrix (criptografia ponta a ponta)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Convide `@zeroclaw:matrix.org` para uma sala criptografada, e o bot responderá com criptografia completa. Veja [Guia Matrix E2EE](docs/matrix-e2ee-guide.md) para configuração de verificação de dispositivo.
|
||||
|
||||
### Multi-Provedor
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover em erro de provedor
|
||||
```
|
||||
|
||||
Se Anthropic falhar ou tiver rate-limit, o orquestrador faz failover automaticamente para OpenAI.
|
||||
|
||||
### Memória Personalizada
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Purga automática após 90 dias
|
||||
```
|
||||
|
||||
Ou use Markdown para armazenamento legível por humanos:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Veja [Referência de Configuração](docs/config-reference.md#memory) para todas as opções de memória.
|
||||
|
||||
## Suporte de Provedor
|
||||
|
||||
| Provedor | Status | API Key | Modelos de Exemplo |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Estável | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Estável | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Estável | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Estável | N/A (local) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Estável | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Estável | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Planejado | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Planejado | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Endpoints Personalizados
|
||||
|
||||
ZeroClaw suporta endpoints compatíveis com OpenAI:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Exemplo: use [LiteLLM](https://github.com/BerriAI/litellm) como proxy para acessar qualquer LLM via interface OpenAI.
|
||||
|
||||
Veja [Referência de Provedores](docs/providers-reference.md) para detalhes de configuração completos.
|
||||
|
||||
## Suporte de Canal
|
||||
|
||||
| Canal | Status | Autenticação | Notas |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Estável | Bot Token | Suporte completo incluindo arquivos, imagens, botões inline |
|
||||
| **Matrix** | ✅ Estável | Senha ou Token | Suporte E2EE com verificação de dispositivo |
|
||||
| **Slack** | 🚧 Planejado | OAuth ou Bot Token | Requer acesso ao workspace |
|
||||
| **Discord** | 🚧 Planejado | Bot Token | Requer permissões de guild |
|
||||
| **WhatsApp** | 🚧 Planejado | Twilio ou API oficial | Requer conta business |
|
||||
| **CLI** | ✅ Estável | Nenhum | Interface conversacional direta |
|
||||
| **Web** | 🚧 Planejado | API Key ou OAuth | Interface de chat baseada em navegador |
|
||||
|
||||
Veja [Referência de Canais](docs/channels-reference.md) para instruções de configuração completas.
|
||||
|
||||
## Suporte de Ferramentas
|
||||
|
||||
ZeroClaw fornece ferramentas integradas para execução de código, acesso ao sistema de arquivos e recuperação web:
|
||||
|
||||
| Ferramenta | Descrição | Runtime Requerido |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Executa comandos shell | Nativo ou Docker |
|
||||
| **python** | Executa scripts Python | Python 3.8+ (nativo) ou Docker |
|
||||
| **javascript** | Executa código Node.js | Node.js 18+ (nativo) ou Docker |
|
||||
| **filesystem_read** | Lê arquivos | Nativo ou Docker |
|
||||
| **filesystem_write** | Escreve arquivos | Nativo ou Docker |
|
||||
| **web_fetch** | Obtém conteúdo web | Nativo ou Docker |
|
||||
|
||||
### Segurança de Execução
|
||||
|
||||
- **Runtime Nativo** — roda como processo de usuário do daemon, acesso completo ao sistema de arquivos
|
||||
- **Runtime Docker** — isolamento completo de container, sistemas de arquivos e redes separados
|
||||
|
||||
Configure a política de execução em `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Lista de permissão explícita
|
||||
```
|
||||
|
||||
Veja [Referência de Configuração](docs/config-reference.md#runtime) para opções de segurança completas.
|
||||
|
||||
## Implantação
|
||||
|
||||
### Implantação Local (Desenvolvimento)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Implantação em Servidor (Produção)
|
||||
|
||||
Use systemd para gerenciar o daemon e agent como serviços:
|
||||
|
||||
```bash
|
||||
# Instale o binário
|
||||
cargo install --path . --locked
|
||||
|
||||
# Configure o workspace
|
||||
zeroclaw init
|
||||
|
||||
# Crie arquivos de serviço systemd
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Habilite e inicie os serviços
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Verifique o status
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Veja [Guia de Implantação de Rede](docs/network-deployment.md) para instruções completas de implantação em produção.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Compile a imagem
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Execute o container
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Veja [`Dockerfile`](Dockerfile) para detalhes de build e opções de configuração.
|
||||
|
||||
### Hardware Edge
|
||||
|
||||
ZeroClaw é projetado para rodar em hardware de baixo consumo:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, núcleo ARMv8 único, < $5 custo de hardware
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, multi-núcleo, ideal para workloads concorrentes
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, quad-core ARMv8, custo ultra-baixo
|
||||
- **SBCs x86 (Intel N100)** — 4-8 GB RAM, builds rápidos, suporte Docker nativo
|
||||
|
||||
Veja [Guia de Hardware](docs/hardware/README.md) para instruções de configuração específicas por dispositivo.
|
||||
|
||||
## Tunneling (Exposição Pública)
|
||||
|
||||
Exponha seu daemon ZeroClaw local à rede pública via túneis seguros:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Provedores de tunnel suportados:
|
||||
|
||||
- **Cloudflare Tunnel** — HTTPS grátis, sem exposição de portas, suporte multi-domínio
|
||||
- **Ngrok** — configuração rápida, domínios personalizados (plano pago)
|
||||
- **Tailscale** — rede mesh privada, sem porta pública
|
||||
|
||||
Veja [Referência de Configuração](docs/config-reference.md#tunnel) para opções de configuração completas.
|
||||
|
||||
## Segurança
|
||||
|
||||
ZeroClaw implementa múltiplas camadas de segurança:
|
||||
|
||||
### Emparelhamento
|
||||
|
||||
O daemon gera um segredo de emparelhamento no primeiro início armazenado em `~/.zeroclaw/workspace/.pairing`. Clientes (agent, CLI) devem apresentar este segredo para conectar.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Gera um novo segredo e invalida o anterior
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Runtime Docker** — isolamento completo de container com sistemas de arquivos e redes separados
|
||||
- **Runtime Nativo** — roda como processo de usuário, com escopo de workspace por padrão
|
||||
|
||||
### Listas de Permissão
|
||||
|
||||
Canais podem restringir acesso por ID de usuário:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Lista de permissão explícita
|
||||
```
|
||||
|
||||
### Criptografia
|
||||
|
||||
- **Matrix E2EE** — criptografia ponta a ponta completa com verificação de dispositivo
|
||||
- **Transporte TLS** — todo o tráfego de API e tunnel usa HTTPS/TLS
|
||||
|
||||
Veja [Documentação de Segurança](docs/security/README.md) para políticas e práticas completas.
|
||||
|
||||
## Observabilidade
|
||||
|
||||
ZeroClaw registra logs em `~/.zeroclaw/workspace/logs/` por padrão. Os logs são armazenados por componente:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Logs do daemon (início, requisições API, erros)
|
||||
├── agent.log # Logs do agent (roteamento de mensagens, execução de ferramentas)
|
||||
├── telegram.log # Logs específicos do canal (se habilitado)
|
||||
└── matrix.log # Logs específicos do canal (se habilitado)
|
||||
```
|
||||
|
||||
### Configuração de Logging
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # Para rotação baseada em tamanho
|
||||
retention_days = 30 # Purga automática após N dias
|
||||
```
|
||||
|
||||
Veja [Referência de Configuração](docs/config-reference.md#logging) para todas as opções de logging.
|
||||
|
||||
### Métricas (Planejado)
|
||||
|
||||
Suporte a métricas Prometheus para monitoramento em produção em breve. Rastreamento em [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Habilidades (Skills)
|
||||
|
||||
ZeroClaw suporta habilidades personalizadas — módulos reutilizáveis que estendem as capacidades do sistema.
|
||||
|
||||
### Definição de Habilidade
|
||||
|
||||
Habilidades são armazenadas em `~/.zeroclaw/workspace/skills/<skill-name>/` com esta estrutura:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Metadados da habilidade (nome, descrição, dependências)
|
||||
├── prompt.md # Prompt de sistema para a AI
|
||||
└── tools/ # Ferramentas personalizadas opcionais
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Exemplo de Habilidade
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Pesquisa na web e resume resultados"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Você é um assistente de pesquisa. Quando pedirem para pesquisar algo:
|
||||
|
||||
1. Use web_fetch para obter o conteúdo
|
||||
2. Resuma os resultados em um formato fácil de ler
|
||||
3. Cite as fontes com URLs
|
||||
```
|
||||
|
||||
### Uso de Habilidades
|
||||
|
||||
Habilidades são carregadas automaticamente no início do agent. Referencie-as por nome em conversas:
|
||||
|
||||
```
|
||||
Usuário: Use a habilidade web-research para encontrar as últimas notícias de AI
|
||||
Bot: [carrega a habilidade web-research, executa web_fetch, resume resultados]
|
||||
```
|
||||
|
||||
Veja seção [Habilidades (Skills)](#habilidades-skills) para instruções completas de criação de habilidades.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw suporta [Open Skills](https://github.com/openagents-com/open-skills) — um sistema modular e agnóstico de provedores para estender capacidades de agentes AI.
|
||||
|
||||
### Habilitar Open Skills
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # opcional
|
||||
```
|
||||
|
||||
Você também pode sobrescrever em runtime com `ZEROCLAW_OPEN_SKILLS_ENABLED` e `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
```bash
|
||||
cargo build # Build de desenvolvimento
|
||||
cargo build --release # Build release (codegen-units=1, funciona em todos os dispositivos incluindo Raspberry Pi)
|
||||
cargo build --profile release-fast # Build mais rápido (codegen-units=8, requer 16 GB+ RAM)
|
||||
cargo test # Executa o suite de testes completo
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Formato
|
||||
|
||||
# Executa o benchmark de comparação SQLite vs Markdown
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Hook pre-push
|
||||
|
||||
Um hook de git executa `cargo fmt --check`, `cargo clippy -- -D warnings`, e `cargo test` antes de cada push. Ative-o uma vez:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Solução de Problemas de Build (erros OpenSSL no Linux)
|
||||
|
||||
Se você encontrar um erro de build `openssl-sys`, sincronize dependências e recompile com o lockfile do repositório:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw está configurado para usar `rustls` para dependências HTTP/TLS; `--locked` mantém o grafo transitivo determinístico em ambientes limpios.
|
||||
|
||||
Para pular o hook quando precisar de um push rápido durante desenvolvimento:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Colaboração e Docs
|
||||
|
||||
Comece com o hub de documentação para um mapa baseado em tarefas:
|
||||
@@ -850,6 +423,20 @@ Um sincero agradecimento às comunidades e instituições que inspiram e aliment
|
||||
|
||||
Construímos em código aberto porque as melhores ideias vêm de todo lugar. Se você está lendo isso, você é parte disso. Bem-vindo. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Repositório Oficial e Aviso de Falsificação
|
||||
|
||||
**Este é o único repositório oficial do ZeroClaw:**
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Ce este ZeroClaw?
|
||||
|
||||
ZeroClaw este o infrastructură de asistent AI ușoară, mutabilă și extensibilă construită în Rust. Conectează diverși furnizori de LLM (Anthropic, OpenAI, Google, Ollama, etc.) printr-o interfață unificată și suportă multiple canale (Telegram, Matrix, CLI, etc.).
|
||||
@@ -177,3 +187,17 @@ Vezi [LICENSE-APACHE](LICENSE-APACHE) și [LICENSE-MIT](LICENSE-MIT) pentru deta
|
||||
Dacă ZeroClaw îți este util, te rugăm să iei în considerare să ne cumperi o cafea:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+26
-100
@@ -53,7 +53,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="install.sh">Установка в 1 клик</a> |
|
||||
<a href="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh">Установка в 1 клик</a> |
|
||||
<a href="docs/setup-guides/README.md">Быстрый старт</a> |
|
||||
<a href="docs/README.ru.md">Хаб документации</a> |
|
||||
<a href="docs/SUMMARY.md">TOC docs</a>
|
||||
@@ -75,6 +75,16 @@
|
||||
>
|
||||
> Последняя синхронизация: **2026-02-19**.
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## 📢 Доска объявлений
|
||||
|
||||
Публикуйте здесь важные уведомления (breaking changes, security advisories, окна обслуживания и блокеры релиза).
|
||||
@@ -163,7 +173,7 @@ cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
|
||||
zeroclaw onboard --api-key sk-... --provider openrouter
|
||||
zeroclaw onboard --interactive
|
||||
zeroclaw onboard
|
||||
|
||||
zeroclaw agent -m "Hello, ZeroClaw!"
|
||||
|
||||
@@ -218,104 +228,6 @@ zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hell
|
||||
zeroclaw agent --provider anthropic -m "hello"
|
||||
```
|
||||
|
||||
## Архитектура
|
||||
|
||||
Каждая подсистема — это **Trait**: меняйте реализации через конфигурацию, без изменения кода.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/architecture.svg" alt="Архитектура ZeroClaw" width="900" />
|
||||
</p>
|
||||
|
||||
| Подсистема | Trait | Встроенные реализации | Расширение |
|
||||
|-----------|-------|---------------------|------------|
|
||||
| **AI-модели** | `Provider` | Каталог через `zeroclaw providers` (сейчас 28 встроенных + алиасы, плюс пользовательские endpoint) | `custom:https://your-api.com` (OpenAI-совместимый) или `anthropic-custom:https://your-api.com` |
|
||||
| **Каналы** | `Channel` | CLI, Telegram, Discord, Slack, Mattermost, iMessage, Matrix, Signal, WhatsApp, Linq, Email, IRC, Lark, DingTalk, QQ, Webhook | Любой messaging API |
|
||||
| **Память** | `Memory` | SQLite гибридный поиск, PostgreSQL-бэкенд, Lucid-мост, Markdown-файлы, явный `none`-бэкенд, snapshot/hydrate, опциональный кэш ответов | Любой persistence-бэкенд |
|
||||
| **Инструменты** | `Tool` | shell/file/memory, cron/schedule, git, pushover, browser, http_request, screenshot/image_info, composio (opt-in), delegate, аппаратные инструменты | Любая функциональность |
|
||||
| **Наблюдаемость** | `Observer` | Noop, Log, Multi | Prometheus, OTel |
|
||||
| **Runtime** | `RuntimeAdapter` | Native, Docker (sandbox) | Через adapter; неподдерживаемые kind завершаются с ошибкой |
|
||||
| **Безопасность** | `SecurityPolicy` | Gateway pairing, sandbox, allowlist, rate limits, scoping файловой системы, шифрование секретов | — |
|
||||
| **Идентификация** | `IdentityConfig` | OpenClaw (markdown), AIEOS v1.1 (JSON) | Любой формат идентификации |
|
||||
| **Туннели** | `Tunnel` | None, Cloudflare, Tailscale, ngrok, Custom | Любой tunnel-бинарник |
|
||||
| **Heartbeat** | Engine | HEARTBEAT.md — периодические задачи | — |
|
||||
| **Навыки** | Loader | TOML-манифесты + SKILL.md-инструкции | Пакеты навыков сообщества |
|
||||
| **Интеграции** | Registry | 70+ интеграций в 9 категориях | Плагинная система |
|
||||
|
||||
### Поддержка runtime (текущая)
|
||||
|
||||
- ✅ Поддерживается сейчас: `runtime.kind = "native"` или `runtime.kind = "docker"`
|
||||
- 🚧 Запланировано, но ещё не реализовано: WASM / edge-runtime
|
||||
|
||||
При указании неподдерживаемого `runtime.kind` ZeroClaw завершается с явной ошибкой, а не молча откатывается к native.
|
||||
|
||||
### Система памяти (полнофункциональный поисковый движок)
|
||||
|
||||
Полностью собственная реализация, ноль внешних зависимостей — без Pinecone, Elasticsearch, LangChain:
|
||||
|
||||
| Уровень | Реализация |
|
||||
|---------|-----------|
|
||||
| **Векторная БД** | Embeddings хранятся как BLOB в SQLite, поиск по косинусному сходству |
|
||||
| **Поиск по ключевым словам** | Виртуальные таблицы FTS5 со скорингом BM25 |
|
||||
| **Гибридное слияние** | Пользовательская взвешенная функция слияния (`vector.rs`) |
|
||||
| **Embeddings** | Trait `EmbeddingProvider` — OpenAI, пользовательский URL или noop |
|
||||
| **Чанкинг** | Построчный Markdown-чанкер с сохранением заголовков |
|
||||
| **Кэширование** | Таблица `embedding_cache` в SQLite с LRU-вытеснением |
|
||||
| **Безопасная переиндексация** | Атомарная перестройка FTS5 + повторное встраивание отсутствующих векторов |
|
||||
|
||||
Agent автоматически вспоминает, сохраняет и управляет памятью через инструменты.
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
backend = "sqlite" # "sqlite", "lucid", "postgres", "markdown", "none"
|
||||
auto_save = true
|
||||
embedding_provider = "none" # "none", "openai", "custom:https://..."
|
||||
vector_weight = 0.7
|
||||
keyword_weight = 0.3
|
||||
```
|
||||
|
||||
## Важные security-дефолты
|
||||
|
||||
- Gateway по умолчанию: `127.0.0.1:42617`
|
||||
- Pairing обязателен по умолчанию: `require_pairing = true`
|
||||
- Публичный bind запрещён по умолчанию: `allow_public_bind = false`
|
||||
- Семантика allowlist каналов:
|
||||
- `[]` => deny-by-default
|
||||
- `["*"]` => allow all (используйте осознанно)
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```toml
|
||||
api_key = "sk-..."
|
||||
default_provider = "openrouter"
|
||||
default_model = "anthropic/claude-sonnet-4-6"
|
||||
default_temperature = 0.7
|
||||
|
||||
[memory]
|
||||
backend = "sqlite"
|
||||
auto_save = true
|
||||
embedding_provider = "none"
|
||||
|
||||
[gateway]
|
||||
host = "127.0.0.1"
|
||||
port = 42617
|
||||
require_pairing = true
|
||||
allow_public_bind = false
|
||||
```
|
||||
|
||||
## Навигация по документации
|
||||
|
||||
- Хаб документации (English): [`docs/README.md`](docs/README.md)
|
||||
- Единый TOC docs: [`docs/SUMMARY.md`](docs/SUMMARY.md)
|
||||
- Хаб документации (Русский): [`docs/README.ru.md`](docs/README.ru.md)
|
||||
- Справочник команд: [`docs/reference/cli/commands-reference.md`](docs/reference/cli/commands-reference.md)
|
||||
- Справочник конфигурации: [`docs/reference/api/config-reference.md`](docs/reference/api/config-reference.md)
|
||||
- Справочник providers: [`docs/reference/api/providers-reference.md`](docs/reference/api/providers-reference.md)
|
||||
- Справочник channels: [`docs/reference/api/channels-reference.md`](docs/reference/api/channels-reference.md)
|
||||
- Операционный runbook: [`docs/ops/operations-runbook.md`](docs/ops/operations-runbook.md)
|
||||
- Устранение неполадок: [`docs/ops/troubleshooting.md`](docs/ops/troubleshooting.md)
|
||||
- Инвентарь и классификация docs: [`docs/maintainers/docs-inventory.md`](docs/maintainers/docs-inventory.md)
|
||||
- Снимок triage проекта: [`docs/maintainers/project-triage-snapshot-2026-02-18.md`](docs/maintainers/project-triage-snapshot-2026-02-18.md)
|
||||
|
||||
## Вклад и лицензия
|
||||
|
||||
- Contribution guide: [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
||||
@@ -323,6 +235,20 @@ allow_public_bind = false
|
||||
- Reviewer playbook: [`docs/contributing/reviewer-playbook.md`](docs/contributing/reviewer-playbook.md)
|
||||
- License: MIT or Apache 2.0 ([`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE), [`NOTICE`](NOTICE))
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
---
|
||||
|
||||
Для полной и исчерпывающей информации (архитектура, все команды, API, разработка) используйте основной английский документ: [`README.md`](README.md).
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Vad är ZeroClaw?
|
||||
|
||||
ZeroClaw är en lättvikts, föränderlig och utökningsbar AI-assistent-infrastruktur byggd i Rust. Den ansluter olika LLM-leverantörer (Anthropic, OpenAI, Google, Ollama, etc.) via ett enhetligt gränssnitt och stöder flera kanaler (Telegram, Matrix, CLI, etc.).
|
||||
@@ -177,3 +187,17 @@ Se [LICENSE-APACHE](LICENSE-APACHE) och [LICENSE-MIT](LICENSE-MIT) för detaljer
|
||||
Om ZeroClaw är användbart för dig, vänligen överväg att köpa en kaffe till oss:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## ZeroClaw คืออะไร?
|
||||
|
||||
ZeroClaw เป็นโครงสร้างพื้นฐานผู้ช่วย AI ที่มีน้ำหนักเบา ปรับเปลี่ยนได้ และขยายได้ สร้างด้วย Rust มันเชื่อมต่อผู้ให้บริการ LLM ต่างๆ (Anthropic, OpenAI, Google, Ollama ฯลฯ) ผ่านอินเทอร์เฟซแบบรวมและรองรับหลายช่องทาง (Telegram, Matrix, CLI ฯลฯ)
|
||||
@@ -177,3 +187,17 @@ channels:
|
||||
หาก ZeroClaw มีประโยชน์สำหรับคุณ โปรดพิจารณาซื้อกาแฟให้เรา:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Binuo ng mga mag-aaral at miyembro ng Harvard, MIT, at Sundai.Club na komunidad.
|
||||
|
||||
<p align="center"><code>Trait-driven architecture · secure-by-default runtime · swappable provider/channel/tool · lahat ay pluggable</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Mga Anunsyo
|
||||
|
||||
Gamitin ang talahanayang ito para sa mahahalagang paunawa (compatibility changes, security notices, maintenance windows, at version blocks).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Nagpapakita ng version at build info
|
||||
|
||||
Tingnan ang [Commands Reference](docs/commands-reference.md) para sa buong options at examples.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Channels (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Custom │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Agent Orchestrator │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Message │ │ Context │ │ Tool │ │
|
||||
│ │ Routing │ │ Memory │ │ Execution │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Providers │ │ Memory │ │ Tools │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ None │ │ Web Fetch │
|
||||
│ Ollama │ │ Custom │ │ Custom │
|
||||
│ Custom │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Runtime (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Mga pangunahing prinsipyo:**
|
||||
|
||||
- Ang lahat ay isang **trait** — providers, channels, tools, memory, tunnels
|
||||
- Ang mga channel ay tumatawag sa orchestrator; ang orchestrator ay tumatawag sa providers + tools
|
||||
- Ang memory system ay nagmamaneho ng conversation context (markdown, SQLite, o none)
|
||||
- Ang runtime ay nag-a-abstract ng code execution (native o Docker)
|
||||
- Walang provider lock-in — i-swap ang Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama nang walang code changes
|
||||
|
||||
Tingnan ang [architecture documentation](docs/architecture.svg) para sa mga detalyadong diagram at implementation details.
|
||||
|
||||
## Mga Halimbawa
|
||||
|
||||
### Telegram Bot
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Ang iyong Telegram user ID
|
||||
```
|
||||
|
||||
Simulan ang daemon + agent, pagkatapos ay magpadala ng mensahe sa iyong bot sa Telegram:
|
||||
|
||||
```
|
||||
/start
|
||||
Hello! Could you help me write a Python script?
|
||||
```
|
||||
|
||||
Ang bot ay tumutugon gamit ang AI-generated code, nagpapatupad ng mga tool kung hiniling, at nagpapanatili ng conversation context.
|
||||
|
||||
### Matrix (end-to-end encryption)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Imbitahan ang `@zeroclaw:matrix.org` sa isang encrypted room, at ang bot ay tutugon gamit ang full encryption. Tingnan ang [Matrix E2EE Guide](docs/matrix-e2ee-guide.md) para sa device verification setup.
|
||||
|
||||
### Multi-Provider
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Failover on provider error
|
||||
```
|
||||
|
||||
Kung ang Anthropic ay mabigo o ma-rate-limit, ang orchestrator ay awtomatikong mag-failover sa OpenAI.
|
||||
|
||||
### Custom Memory
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # Automatic purge after 90 days
|
||||
```
|
||||
|
||||
O gamitin ang Markdown para sa human-readable storage:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Tingnan ang [Configuration Reference](docs/config-reference.md#memory) para sa lahat ng memory options.
|
||||
|
||||
## Provider Support
|
||||
|
||||
| Provider | Status | API Key | Example Models |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Stable | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Stable | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Stable | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Stable | N/A (local) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Stable | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Stable | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Planned | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Planned | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Custom Endpoints
|
||||
|
||||
Sinusuportahan ng ZeroClaw ang OpenAI-compatible endpoints:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Halimbawa: gamitin ang [LiteLLM](https://github.com/BerriAI/litellm) bilang proxy para ma-access ang anumang LLM sa pamamagitan ng OpenAI interface.
|
||||
|
||||
Tingnan ang [Providers Reference](docs/providers-reference.md) para sa kumpletong configuration details.
|
||||
|
||||
## Channel Support
|
||||
|
||||
| Channel | Status | Authentication | Notes |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Stable | Bot Token | Full support including files, images, inline buttons |
|
||||
| **Matrix** | ✅ Stable | Password or Token | E2EE support with device verification |
|
||||
| **Slack** | 🚧 Planned | OAuth or Bot Token | Requires workspace access |
|
||||
| **Discord** | 🚧 Planned | Bot Token | Requires guild permissions |
|
||||
| **WhatsApp** | 🚧 Planned | Twilio or official API | Requires business account |
|
||||
| **CLI** | ✅ Stable | None | Direct conversational interface |
|
||||
| **Web** | 🚧 Planned | API Key or OAuth | Browser-based chat interface |
|
||||
|
||||
Tingnan ang [Channels Reference](docs/channels-reference.md) para sa kumpletong configuration instructions.
|
||||
|
||||
## Tool Support
|
||||
|
||||
Nagbibigay ang ZeroClaw ng built-in tools para sa code execution, filesystem access, at web retrieval:
|
||||
|
||||
| Tool | Description | Required Runtime |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Executes shell commands | Native or Docker |
|
||||
| **python** | Executes Python scripts | Python 3.8+ (native) or Docker |
|
||||
| **javascript** | Executes Node.js code | Node.js 18+ (native) or Docker |
|
||||
| **filesystem_read** | Reads files | Native or Docker |
|
||||
| **filesystem_write** | Writes files | Native or Docker |
|
||||
| **web_fetch** | Fetches web content | Native or Docker |
|
||||
|
||||
### Execution Security
|
||||
|
||||
- **Native Runtime** — runs as daemon's user process, full filesystem access
|
||||
- **Docker Runtime** — full container isolation, separate filesystems and networks
|
||||
|
||||
I-configure ang execution policy sa `config.toml`:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Explicit allowlist
|
||||
```
|
||||
|
||||
Tingnan ang [Configuration Reference](docs/config-reference.md#runtime) para sa kumpletong security options.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Local Deployment (Development)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Server Deployment (Production)
|
||||
|
||||
Gamitin ang systemd para mamaneho ang daemon at agent bilang services:
|
||||
|
||||
```bash
|
||||
# I-install ang binary
|
||||
cargo install --path . --locked
|
||||
|
||||
# I-configure ang workspace
|
||||
zeroclaw init
|
||||
|
||||
# Gumawa ng systemd service files
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# I-enable at i-start ang services
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# I-verify ang status
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Tingnan ang [Network Deployment Guide](docs/network-deployment.md) para sa kumpletong production deployment instructions.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# I-build ang image
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# I-run ang container
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Tingnan ang [`Dockerfile`](Dockerfile) para sa build details at configuration options.
|
||||
|
||||
### Edge Hardware
|
||||
|
||||
Ang ZeroClaw ay dinisenyo para tumakbo sa low-power hardware:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, single ARMv8 core, < $5 hardware cost
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, multi-core, ideal for concurrent workloads
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, quad-core ARMv8, ultra-low cost
|
||||
- **x86 SBCs (Intel N100)** — 4-8 GB RAM, fast builds, native Docker support
|
||||
|
||||
Tingnan ang [Hardware Guide](docs/hardware/README.md) para sa device-specific setup instructions.
|
||||
|
||||
## Tunneling (Public Exposure)
|
||||
|
||||
I-expose ang iyong local ZeroClaw daemon sa public network sa pamamagitan ng secure tunnels:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Mga supported tunnel provider:
|
||||
|
||||
- **Cloudflare Tunnel** — free HTTPS, no port exposure, multi-domain support
|
||||
- **Ngrok** — quick setup, custom domains (paid plan)
|
||||
- **Tailscale** — private mesh network, no public port
|
||||
|
||||
Tingnan ang [Configuration Reference](docs/config-reference.md#tunnel) para sa kumpletong configuration options.
|
||||
|
||||
## Security
|
||||
|
||||
Nagpapatupad ang ZeroClaw ng maraming layer ng security:
|
||||
|
||||
### Pairing
|
||||
|
||||
Ang daemon ay nag-generate ng pairing secret sa unang launch na nakaimbak sa `~/.zeroclaw/workspace/.pairing`. Ang mga client (agent, CLI) ay dapat mag-present ng secret na ito para kumonekta.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Gagawa ng bagong secret at i-invalidate ang dati
|
||||
```
|
||||
|
||||
### Sandboxing
|
||||
|
||||
- **Docker Runtime** — full container isolation na may separate filesystems at networks
|
||||
- **Native Runtime** — runs as user process, scoped sa workspace by default
|
||||
|
||||
### Allowlists
|
||||
|
||||
Ang mga channel ay maaaring mag-limit ng access by user ID:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Explicit allowlist
|
||||
```
|
||||
|
||||
### Encryption
|
||||
|
||||
- **Matrix E2EE** — full end-to-end encryption with device verification
|
||||
- **TLS Transport** — all API and tunnel traffic uses HTTPS/TLS
|
||||
|
||||
Tingnan ang [Security Documentation](docs/security/README.md) para sa kumpletong policies at practices.
|
||||
|
||||
## Observability
|
||||
|
||||
Ang ZeroClaw ay naglo-log sa `~/.zeroclaw/workspace/logs/` by default. Ang mga log ay nakaimbak by component:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Daemon logs (startup, API requests, errors)
|
||||
├── agent.log # Agent logs (message routing, tool execution)
|
||||
├── telegram.log # Channel-specific logs (if enabled)
|
||||
└── matrix.log # Channel-specific logs (if enabled)
|
||||
```
|
||||
|
||||
### Logging Configuration
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # daily, hourly, size
|
||||
max_size_mb = 100 # For size-based rotation
|
||||
retention_days = 30 # Automatic purge after N days
|
||||
```
|
||||
|
||||
Tingnan ang [Configuration Reference](docs/config-reference.md#logging) para sa lahat ng logging options.
|
||||
|
||||
### Metrics (Planned)
|
||||
|
||||
Prometheus metrics support para sa production monitoring ay coming soon. Tracking sa [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234).
|
||||
|
||||
## Skills
|
||||
|
||||
Sinusuportahan ng ZeroClaw ang custom skills — reusable modules na nag-e-extend sa system capabilities.
|
||||
|
||||
### Skill Definition
|
||||
|
||||
Ang mga skill ay nakaimbak sa `~/.zeroclaw/workspace/skills/<skill-name>/` na may ganitong structure:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Skill metadata (name, description, dependencies)
|
||||
├── prompt.md # System prompt for the AI
|
||||
└── tools/ # Optional custom tools
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Skill Example
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Searches the web and summarizes results"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
You are a research assistant. When asked to research something:
|
||||
|
||||
1. Use web_fetch to retrieve content
|
||||
2. Summarize results in an easy-to-read format
|
||||
3. Cite sources with URLs
|
||||
```
|
||||
|
||||
### Skill Usage
|
||||
|
||||
Ang mga skill ay automatically loaded sa agent startup. I-reference ang mga ito by name sa conversations:
|
||||
|
||||
```
|
||||
User: Use the web-research skill to find the latest AI news
|
||||
Bot: [loads web-research skill, executes web_fetch, summarizes results]
|
||||
```
|
||||
|
||||
Tingnan ang [Skills](#skills) section para sa kumpletong skill creation instructions.
|
||||
|
||||
## Open Skills
|
||||
|
||||
Sinusuportahan ng ZeroClaw ang [Open Skills](https://github.com/openagents-com/open-skills) — isang modular at provider-agnostic system para sa pag-extend sa AI agent capabilities.
|
||||
|
||||
### Enable Open Skills
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # optional
|
||||
```
|
||||
|
||||
Maaari mo ring i-override sa runtime gamit ang `ZEROCLAW_OPEN_SKILLS_ENABLED` at `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cargo build # Dev build
|
||||
cargo build --release # Release build (codegen-units=1, works on all devices including Raspberry Pi)
|
||||
cargo build --profile release-fast # Faster build (codegen-units=8, requires 16 GB+ RAM)
|
||||
cargo test # Run full test suite
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Format
|
||||
|
||||
# Run SQLite vs Markdown comparison benchmark
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Pre-push hook
|
||||
|
||||
Ang isang git hook ay nagpapatakbo ng `cargo fmt --check`, `cargo clippy -- -D warnings`, at `cargo test` bago ang bawat push. I-enable ito nang isang beses:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Build Troubleshooting (OpenSSL errors on Linux)
|
||||
|
||||
Kung makakita ka ng `openssl-sys` build error, i-sync ang dependencies at i-recompile gamit ang repository's lockfile:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
Ang ZeroClaw ay naka-configure na gumamit ng `rustls` para sa HTTP/TLS dependencies; ang `--locked` ay nagpapanatili sa transitive graph na deterministic sa clean environments.
|
||||
|
||||
Para i-skip ang hook kapag kailangan mo ng quick push habang nagde-develop:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Collaboration & Docs
|
||||
|
||||
Magsimula sa documentation hub para sa task-based map:
|
||||
@@ -850,6 +423,20 @@ Isang taos-pusong pasasalamat sa mga komunidad at institusyon na nagbibigay-insp
|
||||
|
||||
Kami ay bumubuo sa open source dahil ang mga pinakamahusay na ideya ay nagmumula sa lahat ng dako. Kung binabasa mo ito, ikaw ay bahagi nito. Maligayang pagdating. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Official Repository at Impersonation Warning
|
||||
|
||||
**Ito ang tanging opisyal na ZeroClaw repository:**
|
||||
|
||||
+24
-437
@@ -86,6 +86,16 @@ Harvard, MIT ve Sundai.Club topluluklarının öğrencileri ve üyeleri tarafın
|
||||
|
||||
<p align="center"><code>Trait tabanlı mimari · varsayılan olarak güvenli çalışma zamanı · değiştirilebilir sağlayıcı/kanal/araç · her şey eklenebilir</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Duyurular
|
||||
|
||||
Önemli duyurular için bu tabloyu kullanın (uyumluluk değişiklikleri, güvenlik bildirimleri, bakım pencereleri ve sürüm engellemeleri).
|
||||
@@ -363,443 +373,6 @@ zeroclaw version # Sürüm ve derleme bilgilerini gösterir
|
||||
|
||||
Tam seçenekler ve örnekler için [Komutlar Referansına](docs/commands-reference.md) bakın.
|
||||
|
||||
## Mimari
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Kanallar (trait) │
|
||||
│ Telegram │ Matrix │ Slack │ Discord │ Web │ CLI │ Özel │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Ajan Orkestratörü │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Mesaj │ │ Bağlam │ │ Araç │ │
|
||||
│ │ Yönlendirme│ │ Bellek │ │ Yürütme │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Sağlayıcılar│ │ Bellek │ │ Araçlar │
|
||||
│ (trait) │ │ (trait) │ │ (trait) │
|
||||
├──────────────┤ ├──────────────┤ ├──────────────┤
|
||||
│ Anthropic │ │ Markdown │ │ Filesystem │
|
||||
│ OpenAI │ │ SQLite │ │ Bash │
|
||||
│ Gemini │ │ Yok │ │ Web Fetch │
|
||||
│ Ollama │ │ Özel │ │ Özel │
|
||||
│ Özel │ └──────────────┘ └──────────────┘
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Çalışma Zamanı (trait) │
|
||||
│ Native │ Docker │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Temel ilkeler:**
|
||||
|
||||
- Her şey bir **trait'tir** — sağlayıcılar, kanallar, araçlar, bellek, tüneller
|
||||
- Kanallar orkestratörü çağırır; orkestratör sağlayıcıları + araçları çağırır
|
||||
- Bellek sistemi konuşma bağlamını yönetir (markdown, SQLite veya yok)
|
||||
- Çalışma zamanı kod yürütmeyi soyutlar (yerel veya Docker)
|
||||
- Satıcı kilitlenmesi yok — kod değişikliği olmadan Anthropic ↔ OpenAI ↔ Gemini ↔ Ollama değiştirin
|
||||
|
||||
Detaylı diyagramlar ve uygulama detayları için [mimari belgelerine](docs/architecture.svg) bakın.
|
||||
|
||||
## Örnekler
|
||||
|
||||
### Telegram Bot
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
bot_token = "123456:ABC-DEF..."
|
||||
allowed_users = [987654321] # Telegram kullanıcı ID'niz
|
||||
```
|
||||
|
||||
Arka plan programını + ajanı başlatın, ardından Telegram'da botunuza bir mesaj gönderin:
|
||||
|
||||
```
|
||||
/start
|
||||
Merhaba! Bir Python betiği yazmama yardımcı olabilir misin?
|
||||
```
|
||||
|
||||
Bot, AI tarafından oluşturulan kodla yanıt verir, istenirse araçları yürütür ve konuşma bağlamını korur.
|
||||
|
||||
### Matrix (uçtan uca şifreleme)
|
||||
|
||||
```toml
|
||||
[channels.matrix]
|
||||
enabled = true
|
||||
homeserver_url = "https://matrix.org"
|
||||
username = "@zeroclaw:matrix.org"
|
||||
password = "..."
|
||||
device_name = "zeroclaw-prod"
|
||||
e2ee_enabled = true
|
||||
```
|
||||
|
||||
Şifreli bir odaya `@zeroclaw:matrix.org` davet edin ve bot tam şifrelemeyle yanıt verecektir. Cihaz doğrulama kurulumu için [Matrix E2EE Kılavuzuna](docs/matrix-e2ee-guide.md) bakın.
|
||||
|
||||
### Çoklu-Sağlayıcı
|
||||
|
||||
```toml
|
||||
[providers.anthropic]
|
||||
enabled = true
|
||||
api_key = "sk-ant-..."
|
||||
model = "claude-sonnet-4-20250514"
|
||||
|
||||
[providers.openai]
|
||||
enabled = true
|
||||
api_key = "sk-..."
|
||||
model = "gpt-4o"
|
||||
|
||||
[orchestrator]
|
||||
default_provider = "anthropic"
|
||||
fallback_providers = ["openai"] # Sağlayıcı hatasında geçiş
|
||||
```
|
||||
|
||||
Anthropic başarısız olursa veya hız sınırına ulaşırsa, orkestratör otomatik olarak OpenAI'ya geçer.
|
||||
|
||||
### Özel Bellek
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "sqlite"
|
||||
path = "~/.zeroclaw/workspace/memory/conversations.db"
|
||||
retention_days = 90 # 90 gün sonra otomatik temizleme
|
||||
```
|
||||
|
||||
Veya insan tarafından okunabilir depolama için Markdown kullanın:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
kind = "markdown"
|
||||
path = "~/.zeroclaw/workspace/memory/"
|
||||
```
|
||||
|
||||
Tüm bellek seçenekleri için [Yapılandırma Referansına](docs/config-reference.md#memory) bakın.
|
||||
|
||||
## Sağlayıcı Desteği
|
||||
|
||||
| Sağlayıcı | Durum | API Anahtarı | Örnek Modeller |
|
||||
| ----------------- | ----------- | ------------------- | ---------------------------------------------------- |
|
||||
| **Anthropic** | ✅ Kararlı | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514`, `claude-opus-4-20250514` |
|
||||
| **OpenAI** | ✅ Kararlı | `OPENAI_API_KEY` | `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini` |
|
||||
| **Google Gemini** | ✅ Kararlı | `GOOGLE_API_KEY` | `gemini-2.0-flash-exp`, `gemini-exp-1206` |
|
||||
| **Ollama** | ✅ Kararlı | Yok (yerel) | `llama3.3`, `qwen2.5`, `phi4` |
|
||||
| **Cerebras** | ✅ Kararlı | `CEREBRAS_API_KEY` | `llama-3.3-70b` |
|
||||
| **Groq** | ✅ Kararlı | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||
| **Mistral** | 🚧 Planlanan | `MISTRAL_API_KEY` | TBD |
|
||||
| **Cohere** | 🚧 Planlanan | `COHERE_API_KEY` | TBD |
|
||||
|
||||
### Özel Uç Noktalar
|
||||
|
||||
ZeroClaw, OpenAI uyumlu uç noktaları destekler:
|
||||
|
||||
```toml
|
||||
[providers.custom]
|
||||
enabled = true
|
||||
api_key = "..."
|
||||
base_url = "https://api.your-llm-provider.com/v1"
|
||||
model = "your-model-name"
|
||||
```
|
||||
|
||||
Örnek: herhangi bir LLM'ye OpenAI arayüzü üzerinden erişmek için [LiteLLM](https://github.com/BerriAI/litellm)'i proxy olarak kullanın.
|
||||
|
||||
Tam yapılandırma detayları için [Sağlayıcı Referansına](docs/providers-reference.md) bakın.
|
||||
|
||||
## Kanal Desteği
|
||||
|
||||
| Kanal | Durum | Kimlik Doğrulama | Notlar |
|
||||
| ------------ | ----------- | ------------------------ | --------------------------------------------------------- |
|
||||
| **Telegram** | ✅ Kararlı | Bot Token | Dosyalar, resimler, satır içi düğmeler dahil tam destek |
|
||||
| **Matrix** | ✅ Kararlı | Şifre veya Token | Cihaz doğrulamalı E2EE desteği |
|
||||
| **Slack** | 🚧 Planlanan | OAuth veya Bot Token | Çalışma alanı erişimi gerektirir |
|
||||
| **Discord** | 🚧 Planlanan | Bot Token | Guild izinleri gerektirir |
|
||||
| **WhatsApp** | 🚧 Planlanan | Twilio veya resmi API | İş hesabı gerektirir |
|
||||
| **CLI** | ✅ Kararlı | Yok | Doğrudan konuşma arayüzü |
|
||||
| **Web** | 🚧 Planlanan | API Anahtarı veya OAuth | Tarayıcı tabanlı sohbet arayüzü |
|
||||
|
||||
Tam yapılandırma talimatları için [Kanallar Referansına](docs/channels-reference.md) bakın.
|
||||
|
||||
## Araç Desteği
|
||||
|
||||
ZeroClaw, kod yürütme, dosya sistemi erişimi ve web alımı için yerleşik araçlar sağlar:
|
||||
|
||||
| Araç | Açıklama | Gerekli Çalışma Zamanı |
|
||||
| -------------------- | --------------------------- | ----------------------------- |
|
||||
| **bash** | Shell komutlarını yürüt | Yerel veya Docker |
|
||||
| **python** | Python betiklerini yürüt | Python 3.8+ (yerel) veya Docker |
|
||||
| **javascript** | Node.js kodunu yürüt | Node.js 18+ (yerel) veya Docker |
|
||||
| **filesystem_read** | Dosyaları oku | Yerel veya Docker |
|
||||
| **filesystem_write** | Dosyaları yaz | Yerel veya Docker |
|
||||
| **web_fetch** | Web içeriği al | Yerel veya Docker |
|
||||
|
||||
### Yürütme Güvenliği
|
||||
|
||||
- **Yerel Çalışma Zamanı** — arka plan programının kullanıcı süreci olarak çalışır, tam dosya sistemi erişimi
|
||||
- **Docker Çalışma Zamanı** — tam konteyner yalıtımı, ayrı dosya sistemleri ve ağlar
|
||||
|
||||
`config.toml` içinde yürütme ilkesini yapılandırın:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "docker"
|
||||
allowed_tools = ["bash", "python", "filesystem_read"] # Açık izin listesi
|
||||
```
|
||||
|
||||
Tam güvenlik seçenekleri için [Yapılandırma Referansına](docs/config-reference.md#runtime) bakın.
|
||||
|
||||
## Dağıtım
|
||||
|
||||
### Yerel Dağıtım (Geliştirme)
|
||||
|
||||
```bash
|
||||
zeroclaw daemon start
|
||||
zeroclaw agent start
|
||||
```
|
||||
|
||||
### Sunucu Dağıtımı (Üretim)
|
||||
|
||||
Arka plan programını ve ajanı hizmet olarak yönetmek için systemd kullanın:
|
||||
|
||||
```bash
|
||||
# İkiliyi yükle
|
||||
cargo install --path . --locked
|
||||
|
||||
# Çalışma alanını yapılandır
|
||||
zeroclaw init
|
||||
|
||||
# systemd hizmet dosyaları oluştur
|
||||
sudo cp deployment/systemd/zeroclaw-daemon.service /etc/systemd/system/
|
||||
sudo cp deployment/systemd/zeroclaw-agent.service /etc/systemd/system/
|
||||
|
||||
# Hizmetleri etkinleştir ve başlat
|
||||
sudo systemctl enable zeroclaw-daemon zeroclaw-agent
|
||||
sudo systemctl start zeroclaw-daemon zeroclaw-agent
|
||||
|
||||
# Durumu doğrula
|
||||
sudo systemctl status zeroclaw-daemon
|
||||
sudo systemctl status zeroclaw-agent
|
||||
```
|
||||
|
||||
Tam üretim dağıtım talimatları için [Ağ Dağıtımı Kılavuzuna](docs/network-deployment.md) bakın.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# İmajı oluştur
|
||||
docker build -t zeroclaw:latest .
|
||||
|
||||
# Konteyneri çalıştır
|
||||
docker run -d \
|
||||
--name zeroclaw \
|
||||
-v ~/.zeroclaw/workspace:/workspace \
|
||||
-e ANTHROPIC_API_KEY=sk-ant-... \
|
||||
zeroclaw:latest
|
||||
```
|
||||
|
||||
Derleme detayları ve yapılandırma seçenekleri için [`Dockerfile`](Dockerfile)'a bakın.
|
||||
|
||||
### Uç Donanım
|
||||
|
||||
ZeroClaw, düşük güç tüketimli donanımda çalışmak üzere tasarlanmıştır:
|
||||
|
||||
- **Raspberry Pi Zero 2 W** — ~512 MB RAM, tek ARMv8 çekirdek, < $5 donanım maliyeti
|
||||
- **Raspberry Pi 4/5** — 1 GB+ RAM, çok çekirdekli, eşzamanlı iş yükleri için ideal
|
||||
- **Orange Pi Zero 2** — ~512 MB RAM, dört çekirdekli ARMv8, ultra düşük maliyet
|
||||
- **x86 SBC'ler (Intel N100)** — 4-8 GB RAM, hızlı derlemeler, yerel Docker desteği
|
||||
|
||||
Cihaza özgü kurulum talimatları için [Donanım Kılavuzuna](docs/hardware/README.md) bakın.
|
||||
|
||||
## Tünelleme (Herkese Açık Kullanım)
|
||||
|
||||
Yerel ZeroClaw arka plan programınızı güvenli tüneller aracılığıyla herkese açık ağa çıkarın:
|
||||
|
||||
```bash
|
||||
zeroclaw tunnel start --provider cloudflare
|
||||
```
|
||||
|
||||
Desteklenen tünel sağlayıcıları:
|
||||
|
||||
- **Cloudflare Tunnel** — ücretsiz HTTPS, port açığa çıkarma yok, çoklu etki alanı desteği
|
||||
- **Ngrok** — hızlı kurulum, özel etki alanları (ücretli plan)
|
||||
- **Tailscale** — özel mesh ağı. herkese açık port yok
|
||||
|
||||
Tam yapılandırma seçenekleri için [Yapılandırma Referansına](docs/config-reference.md#tunnel) bakın.
|
||||
|
||||
## Güvenlik
|
||||
|
||||
ZeroClaw birden çok güvenlik katmanı uygular:
|
||||
|
||||
### Eşleştirme
|
||||
|
||||
Arka plan programı ilk başlangıçta `~/.zeroclaw/workspace/.pairing` içinde saklanan bir eşleştirme sırrı oluşturur. İstemciler (ajan, CLI) bağlanmak için bu sırrı sunmalıdır.
|
||||
|
||||
```bash
|
||||
zeroclaw pairing rotate # Yeni bir sır oluşturur ve eskisini geçersiz kılar
|
||||
```
|
||||
|
||||
### Kum Alanı
|
||||
|
||||
- **Docker Çalışma Zamanı** — ayrı dosya sistemleri ve ağlarla tam konteyner yalıtımı
|
||||
- **Yerel Çalışma Zamanı** — kullanıcı süreci olarak çalışır; varsayılan olarak çalışma alanıyla sınırlıdır
|
||||
|
||||
### İzin Listeleri
|
||||
|
||||
Kanallar kullanıcı ID'sine göre erişimi kısıtlayabilir:
|
||||
|
||||
```toml
|
||||
[channels.telegram]
|
||||
enabled = true
|
||||
allowed_users = [123456789, 987654321] # Açık izin listesi
|
||||
```
|
||||
|
||||
### Şifreleme
|
||||
|
||||
- **Matrix E2EE** — cihaz doğrulamalı tam uçtan uca şifreleme
|
||||
- **TLS Taşıma** — tüm API ve tünel trafiği HTTPS/TLS kullanır
|
||||
|
||||
Tam ilkeler ve uygulamalar için [Güvenlik Belgelerine](docs/security/README.md) bakın.
|
||||
|
||||
## Gözlemlenebilirlik
|
||||
|
||||
ZeroClaw varsayılan olarak `~/.zeroclaw/workspace/logs/` dizinine log yazar. Loglar bileşene göre saklanır:
|
||||
|
||||
```
|
||||
~/.zeroclaw/workspace/logs/
|
||||
├── daemon.log # Arka plan programı logları (başlangıç, API istekleri, hatalar)
|
||||
├── agent.log # Ajan logları (mesaj yönlendirme, araç yürütme)
|
||||
├── telegram.log # Kanala özgü loglar (etkinse)
|
||||
└── matrix.log # Kanala özgü loglar (etkinse)
|
||||
```
|
||||
|
||||
### Loglama Yapılandırması
|
||||
|
||||
```toml
|
||||
[logging]
|
||||
level = "info" # debug, info, warn, error
|
||||
path = "~/.zeroclaw/workspace/logs/"
|
||||
rotation = "daily" # günlük, saatlik, boyut
|
||||
max_size_mb = 100 # Boyut tabanlı döndürme için
|
||||
retention_days = 30 # N gün sonra otomatik temizleme
|
||||
```
|
||||
|
||||
Tüm loglama seçenekleri için [Yapılandırma Referansına](docs/config-reference.md#logging) bakın.
|
||||
|
||||
### Metrikler (Planlanan)
|
||||
|
||||
Üretim izleme için Prometheus metrikleri desteği yakında geliyor. [#234](https://github.com/zeroclaw-labs/zeroclaw/issues/234) numaralı konuda takip ediliyor.
|
||||
|
||||
## Beceriler
|
||||
|
||||
ZeroClaw, sistem yeteneklerini genişleten yeniden kullanılabilir modüller olan özel becerileri destekler.
|
||||
|
||||
### Beceri Tanımı
|
||||
|
||||
Beceriler bu yapı ile `~/.zeroclaw/workspace/skills/<skill-name>/` içinde saklanır:
|
||||
|
||||
```
|
||||
skills/
|
||||
└── my-skill/
|
||||
├── skill.toml # Beceri metaverileri (ad, açıklama, bağımlılıklar)
|
||||
├── prompt.md # AI için sistem istemi
|
||||
└── tools/ # İsteğe bağlı özel araçlar
|
||||
└── my_tool.py
|
||||
```
|
||||
|
||||
### Beceri Örneği
|
||||
|
||||
```toml
|
||||
# skills/web-research/skill.toml
|
||||
[skill]
|
||||
name = "web-research"
|
||||
description = "Web'de arama yapar ve sonuçları özetler"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
tools = ["web_fetch", "bash"]
|
||||
```
|
||||
|
||||
```markdown
|
||||
<!-- skills/web-research/prompt.md -->
|
||||
|
||||
Sen bir araştırma asistanısın. Bir şeyi araştırmam istendiğinde:
|
||||
|
||||
1. İçeriği almak için web_fetch kullan
|
||||
2. Sonuçları okunması kolay bir biçimde özetle
|
||||
3. Kaynakları URL'lerle göster
|
||||
```
|
||||
|
||||
### Beceri Kullanımı
|
||||
|
||||
Beceriler ajan başlangıcında otomatik olarak yüklenir. Konuşmalarda ada göre başvurun:
|
||||
|
||||
```
|
||||
Kullanıcı: En son AI haberlerini bulmak için web-research becerisini kullan
|
||||
Bot: [web-research becerisini yükler, web_fetch'i yürütür, sonuçları özetler]
|
||||
```
|
||||
|
||||
Tam beceri oluşturma talimatları için [Beceriler](#beceriler) bölümüne bakın.
|
||||
|
||||
## Open Skills
|
||||
|
||||
ZeroClaw, AI ajan yeteneklerini genişletmek için modüler ve sağlayıcıdan bağımsız bir sistem olan [Open Skills](https://github.com/openagents-com/open-skills)'i destekler.
|
||||
|
||||
### Open Skills'i Etkinleştir
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # isteğe bağlı
|
||||
```
|
||||
|
||||
Ayrıca `ZEROCLAW_OPEN_SKILLS_ENABLED` ve `ZEROCLAW_OPEN_SKILLS_DIR` ile çalışma zamanında geçersiz kılabilirsiniz.
|
||||
|
||||
## Geliştirme
|
||||
|
||||
```bash
|
||||
cargo build # Geliştirme derlemesi
|
||||
cargo build --release # Sürüm derlemesi (codegen-units=1, Raspberry Pi dahil tüm cihazlarda çalışır)
|
||||
cargo build --profile release-fast # Daha hızlı derleme (codegen-units=8, 16 GB+ RAM gerektirir)
|
||||
cargo test # Tam test paketini çalıştır
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Biçimlendir
|
||||
|
||||
# SQLite vs Markdown karşılaştırma kıyaslamasını çalıştır
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Ön push kancası
|
||||
|
||||
Bir git kancası her push'tan önce `cargo fmt --check`, `cargo clippy -- -D warnings` ve `cargo test` çalıştırır. Bir kez etkinleştirin:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Derleme Sorun Giderme (Linux'ta OpenSSL hataları)
|
||||
|
||||
Bir `openssl-sys` derleme hatasıyla karşılaşırsanız, bağımlılıkları eşzamanlayın ve deponun lockfile'ı ile yeniden derleyin:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw, HTTP/TLS bağımlılıkları için `rustls` kullanacak şekilde yapılandırılmıştır; `--locked`, geçişli grafiği temiz ortamlarda deterministik tutar.
|
||||
|
||||
Geliştirme sırasında hızlı bir push'a ihtiyacınız olduğunda kancayı atlamak için:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## İşbirliği ve Belgeler
|
||||
|
||||
Görev tabanlı bir harita için belge merkeziyle başlayın:
|
||||
@@ -850,6 +423,20 @@ Bu açık kaynak çalışmasını ilham veren ve besleyen topluluklara ve kuruml
|
||||
|
||||
En iyi fikirler her yerden geldiği için açık kaynakta inşa ediyoruz. Bunu okuyorsan, bunun bir parçasısın. Hoş geldin. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Resmi Depo ve Taklit Uyarısı
|
||||
|
||||
**Bu tek resmi ZeroClaw deposudur:**
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## Що таке ZeroClaw?
|
||||
|
||||
ZeroClaw — це легка, змінювана та розширювана інфраструктура AI-асистента, написана на Rust. Вона з'єднує різних LLM-провайдерів (Anthropic, OpenAI, Google, Ollama тощо) через уніфікований інтерфейс і підтримує багато каналів (Telegram, Matrix, CLI тощо).
|
||||
@@ -177,3 +187,17 @@ channels:
|
||||
Якщо ZeroClaw корисний для вас, будь ласка, розгляньте можливість купити нам каву:
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## ZeroClaw کیا ہے؟
|
||||
|
||||
<p align="center" dir="rtl">
|
||||
@@ -195,3 +205,17 @@ channels:
|
||||
</p>
|
||||
|
||||
[](https://buymeacoffee.com/argenistherose)
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
+28
-574
@@ -58,7 +58,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="#quick-start">Bắt đầu</a> |
|
||||
<a href="install.sh">Cài đặt một lần bấm</a> |
|
||||
<a href="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh">Cài đặt một lần bấm</a> |
|
||||
<a href="docs/i18n/vi/README.md">Trung tâm tài liệu</a> |
|
||||
<a href="docs/SUMMARY.md">Mục lục tài liệu</a>
|
||||
</p>
|
||||
@@ -84,6 +84,16 @@
|
||||
|
||||
<p align="center"><code>Kiến trúc trait-driven · mặc định bảo mật · provider/channel/tool hoán đổi tự do · mọi thứ đều dễ mở rộng</code></p>
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
### 📢 Thông báo
|
||||
|
||||
Bảng này dành cho các thông báo quan trọng (thay đổi không tương thích, cảnh báo bảo mật, lịch bảo trì, vấn đề chặn release).
|
||||
@@ -260,7 +270,7 @@ cd zeroclaw
|
||||
./install.sh --prebuilt-only
|
||||
|
||||
# Tùy chọn: chạy onboarding trong cùng luồng
|
||||
./install.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"]
|
||||
./install.sh --api-key "sk-..." --provider openrouter [--model "openrouter/auto"]
|
||||
|
||||
# Tùy chọn: chạy bootstrap + onboarding hoàn toàn ở chế độ tương thích với Docker
|
||||
./install.sh --docker
|
||||
@@ -311,8 +321,8 @@ export PATH="$HOME/.cargo/bin:$PATH"
|
||||
# Cài nhanh (không cần tương tác, có thể chỉ định model)
|
||||
zeroclaw onboard --api-key sk-... --provider openrouter [--model "openrouter/auto"]
|
||||
|
||||
# Hoặc dùng trình hướng dẫn tương tác
|
||||
zeroclaw onboard --interactive
|
||||
# Hoặc dùng trình hướng dẫn
|
||||
zeroclaw onboard
|
||||
|
||||
# Hoặc chỉ sửa nhanh channel/allowlist
|
||||
zeroclaw onboard --channels-only
|
||||
@@ -406,576 +416,6 @@ zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hell
|
||||
zeroclaw agent --provider anthropic -m "hello"
|
||||
```
|
||||
|
||||
## Kiến trúc
|
||||
|
||||
Mọi hệ thống con đều là **trait** — chỉ cần đổi cấu hình, không cần sửa code.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/architecture.svg" alt="ZeroClaw Architecture" width="900" />
|
||||
</p>
|
||||
|
||||
| Hệ thống con | Trait | Đi kèm sẵn | Mở rộng |
|
||||
|-----------|-------|------------|--------|
|
||||
| **Mô hình AI** | `Provider` | Danh mục provider qua `zeroclaw providers` (hiện có 28 built-in + alias, cộng endpoint tùy chỉnh) | `custom:https://your-api.com` (tương thích OpenAI) hoặc `anthropic-custom:https://your-api.com` |
|
||||
| **Channel** | `Channel` | CLI, Telegram, Discord, Slack, Mattermost, iMessage, Matrix, Signal, WhatsApp, Linq, Email, IRC, Lark, DingTalk, QQ, Webhook | Bất kỳ messaging API nào |
|
||||
| **Memory** | `Memory` | SQLite hybrid search, PostgreSQL backend (storage provider có thể cấu hình), Lucid bridge, Markdown files, backend `none` tường minh, snapshot/hydrate, response cache tùy chọn | Bất kỳ persistence backend nào |
|
||||
| **Tool** | `Tool` | shell/file/memory, cron/schedule, git, pushover, browser, http_request, screenshot/image_info, composio (opt-in), delegate, hardware tools | Bất kỳ khả năng nào |
|
||||
| **Observability** | `Observer` | Noop, Log, Multi | Prometheus, OTel |
|
||||
| **Runtime** | `RuntimeAdapter` | Native, Docker (sandboxed) | Có thể thêm runtime bổ sung qua adapter; các kind không được hỗ trợ sẽ fail nhanh |
|
||||
| **Bảo mật** | `SecurityPolicy` | Ghép cặp gateway, sandbox, allowlist, giới hạn tốc độ, phân vùng filesystem, secret mã hóa | — |
|
||||
| **Định danh** | `IdentityConfig` | OpenClaw (markdown), AIEOS v1.1 (JSON) | Bất kỳ định dạng định danh nào |
|
||||
| **Tunnel** | `Tunnel` | None, Cloudflare, Tailscale, ngrok, Custom | Bất kỳ tunnel binary nào |
|
||||
| **Heartbeat** | Engine | Tác vụ định kỳ HEARTBEAT.md | — |
|
||||
| **Skill** | Loader | TOML manifest + hướng dẫn SKILL.md | Community skill pack |
|
||||
| **Tích hợp** | Registry | 70+ tích hợp trong 9 danh mục | Plugin system |
|
||||
|
||||
### Hỗ trợ runtime (hiện tại)
|
||||
|
||||
- ✅ Được hỗ trợ hiện nay: `runtime.kind = "native"` hoặc `runtime.kind = "docker"`
|
||||
- 🚧 Đã lên kế hoạch, chưa triển khai: WASM / edge runtime
|
||||
|
||||
Khi cấu hình `runtime.kind` không được hỗ trợ, ZeroClaw sẽ thoát với thông báo lỗi rõ ràng thay vì âm thầm fallback về native.
|
||||
|
||||
### Hệ thống Memory (Search Engine toàn diện)
|
||||
|
||||
Tự phát triển hoàn toàn, không phụ thuộc bên ngoài — không Pinecone, không Elasticsearch, không LangChain:
|
||||
|
||||
| Lớp | Triển khai |
|
||||
|-------|---------------|
|
||||
| **Vector DB** | Embeddings lưu dưới dạng BLOB trong SQLite, tìm kiếm cosine similarity |
|
||||
| **Keyword Search** | Bảng ảo FTS5 với BM25 scoring |
|
||||
| **Hybrid Merge** | Hàm merge có trọng số tùy chỉnh (`vector.rs`) |
|
||||
| **Embeddings** | Trait `EmbeddingProvider` — OpenAI, URL tùy chỉnh, hoặc noop |
|
||||
| **Chunking** | Bộ chia đoạn markdown theo dòng, giữ nguyên heading |
|
||||
| **Caching** | Bảng SQLite `embedding_cache` với LRU eviction |
|
||||
| **Safe Reindex** | Rebuild FTS5 + re-embed các vector bị thiếu theo cách nguyên tử |
|
||||
|
||||
Agent tự động ghi nhớ, lưu trữ và quản lý memory qua các tool.
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
backend = "sqlite" # "sqlite", "lucid", "postgres", "markdown", "none"
|
||||
auto_save = true
|
||||
embedding_provider = "none" # "none", "openai", "custom:https://..."
|
||||
vector_weight = 0.7
|
||||
keyword_weight = 0.3
|
||||
|
||||
# backend = "none" sử dụng no-op memory backend tường minh (không có persistence)
|
||||
|
||||
# Tùy chọn: ghi đè storage-provider cho remote memory backend.
|
||||
# Khi provider = "postgres", ZeroClaw dùng PostgreSQL để lưu memory.
|
||||
# Khóa db_url cũng chấp nhận alias `dbURL` để tương thích ngược.
|
||||
#
|
||||
# [storage.provider.config]
|
||||
# provider = "postgres"
|
||||
# db_url = "postgres://user:password@host:5432/zeroclaw"
|
||||
# schema = "public"
|
||||
# table = "memories"
|
||||
# connect_timeout_secs = 15
|
||||
|
||||
# Tùy chọn cho backend = "sqlite": số giây tối đa chờ khi mở DB (ví dụ: file bị khóa). Bỏ qua hoặc để trống để không có timeout.
|
||||
# sqlite_open_timeout_secs = 30
|
||||
|
||||
# Tùy chọn cho backend = "lucid"
|
||||
# ZEROCLAW_LUCID_CMD=/usr/local/bin/lucid # mặc định: lucid
|
||||
# ZEROCLAW_LUCID_BUDGET=200 # mặc định: 200
|
||||
# ZEROCLAW_LUCID_LOCAL_HIT_THRESHOLD=3 # số lần hit cục bộ để bỏ qua external recall
|
||||
# ZEROCLAW_LUCID_RECALL_TIMEOUT_MS=120 # giới hạn thời gian cho lucid context recall
|
||||
# ZEROCLAW_LUCID_STORE_TIMEOUT_MS=800 # timeout đồng bộ async cho lucid store
|
||||
# ZEROCLAW_LUCID_FAILURE_COOLDOWN_MS=15000 # thời gian nghỉ sau lỗi lucid, tránh thử lại liên tục
|
||||
```
|
||||
|
||||
## Bảo mật
|
||||
|
||||
ZeroClaw thực thi bảo mật ở **mọi lớp** — không chỉ sandbox. Đáp ứng tất cả các hạng mục trong danh sách kiểm tra bảo mật của cộng đồng.
|
||||
|
||||
### Danh sách kiểm tra bảo mật
|
||||
|
||||
| # | Hạng mục | Trạng thái | Cách thực hiện |
|
||||
|---|------|--------|-----|
|
||||
| 1 | **Gateway không công khai ra ngoài** | ✅ | Bind vào `127.0.0.1` theo mặc định. Từ chối `0.0.0.0` nếu không có tunnel hoặc `allow_public_bind = true` tường minh. |
|
||||
| 2 | **Yêu cầu ghép cặp** | ✅ | Mã một lần 6 chữ số khi khởi động. Trao đổi qua `POST /pair` để lấy bearer token. Mọi yêu cầu `/webhook` đều cần `Authorization: Bearer <token>`. |
|
||||
| 3 | **Phân vùng filesystem (không phải /)** | ✅ | `workspace_only = true` theo mặc định. Chặn 14 thư mục hệ thống + 4 dotfile nhạy cảm. Chặn null byte injection. Phát hiện symlink escape qua canonicalization + kiểm tra resolved-path trong các tool đọc/ghi file. |
|
||||
| 4 | **Chỉ truy cập qua tunnel** | ✅ | Gateway từ chối bind công khai khi không có tunnel đang hoạt động. Hỗ trợ Tailscale, Cloudflare, ngrok, hoặc tunnel tùy chỉnh. |
|
||||
|
||||
> **Tự chạy nmap:** `nmap -p 1-65535 <your-host>` — ZeroClaw chỉ bind vào localhost, nên không có gì bị lộ ra ngoài trừ khi bạn cấu hình tunnel tường minh.
|
||||
|
||||
### Allowlist channel (từ chối theo mặc định)
|
||||
|
||||
Chính sách kiểm soát người gửi đã được thống nhất:
|
||||
|
||||
- Allowlist rỗng = **từ chối tất cả tin nhắn đến**
|
||||
- `"*"` = **cho phép tất cả** (phải opt-in tường minh)
|
||||
- Nếu khác = allowlist khớp chính xác
|
||||
|
||||
Mặc định an toàn, hạn chế tối đa rủi ro lộ thông tin.
|
||||
|
||||
Tài liệu tham khảo đầy đủ về cấu hình channel: [docs/reference/api/channels-reference.md](docs/reference/api/channels-reference.md).
|
||||
|
||||
Cài đặt được khuyến nghị (bảo mật + nhanh):
|
||||
|
||||
- **Telegram:** thêm `@username` của bạn (không có `@`) và/hoặc Telegram user ID số vào allowlist.
|
||||
- **Discord:** thêm Discord user ID của bạn vào allowlist.
|
||||
- **Slack:** thêm Slack member ID của bạn (thường bắt đầu bằng `U`) vào allowlist.
|
||||
- **Mattermost:** dùng API v4 tiêu chuẩn. Allowlist dùng Mattermost user ID.
|
||||
- Chỉ dùng `"*"` cho kiểm thử mở tạm thời.
|
||||
|
||||
Luồng phê duyệt của operator qua Telegram:
|
||||
|
||||
1. Để `[channels_config.telegram].allowed_users = []` để từ chối theo mặc định khi khởi động.
|
||||
2. Người dùng không được phép sẽ nhận được gợi ý kèm lệnh operator có thể copy:
|
||||
`zeroclaw channel bind-telegram <IDENTITY>`.
|
||||
3. Operator chạy lệnh đó tại máy cục bộ, sau đó người dùng thử gửi tin nhắn lại.
|
||||
|
||||
Nếu cần phê duyệt thủ công một lần, chạy:
|
||||
|
||||
```bash
|
||||
zeroclaw channel bind-telegram 123456789
|
||||
```
|
||||
|
||||
Nếu bạn không chắc định danh nào cần dùng:
|
||||
|
||||
1. Khởi động channel và gửi một tin nhắn đến bot của bạn.
|
||||
2. Đọc log cảnh báo để thấy định danh người gửi chính xác.
|
||||
3. Thêm giá trị đó vào allowlist và chạy lại channel-only setup.
|
||||
|
||||
Nếu bạn thấy cảnh báo ủy quyền trong log (ví dụ: `ignoring message from unauthorized user`),
|
||||
chạy lại channel setup:
|
||||
|
||||
```bash
|
||||
zeroclaw onboard --channels-only
|
||||
```
|
||||
|
||||
### Phản hồi media Telegram
|
||||
|
||||
Telegram định tuyến phản hồi theo **chat ID nguồn** (thay vì username),
|
||||
tránh lỗi `Bad Request: chat not found`.
|
||||
|
||||
Với các phản hồi không phải văn bản, ZeroClaw có thể gửi file đính kèm Telegram khi assistant bao gồm các marker:
|
||||
|
||||
- `[IMAGE:<path-or-url>]`
|
||||
- `[DOCUMENT:<path-or-url>]`
|
||||
- `[VIDEO:<path-or-url>]`
|
||||
- `[AUDIO:<path-or-url>]`
|
||||
- `[VOICE:<path-or-url>]`
|
||||
|
||||
Path có thể là file cục bộ (ví dụ `/tmp/screenshot.png`) hoặc URL HTTPS.
|
||||
|
||||
### Cài đặt WhatsApp
|
||||
|
||||
ZeroClaw hỗ trợ hai backend WhatsApp:
|
||||
|
||||
- **Chế độ WhatsApp Web** (QR / pair code, không cần Meta Business API)
|
||||
- **Chế độ WhatsApp Business Cloud API** (luồng webhook chính thức của Meta)
|
||||
|
||||
#### Chế độ WhatsApp Web (khuyến nghị cho dùng cá nhân/self-hosted)
|
||||
|
||||
1. **Build với hỗ trợ WhatsApp Web:**
|
||||
```bash
|
||||
cargo build --features whatsapp-web
|
||||
```
|
||||
|
||||
2. **Cấu hình ZeroClaw:**
|
||||
```toml
|
||||
[channels_config.whatsapp]
|
||||
session_path = "~/.zeroclaw/state/whatsapp-web/session.db"
|
||||
pair_phone = "15551234567" # tùy chọn; bỏ qua để dùng luồng QR
|
||||
pair_code = "" # tùy chọn mã pair tùy chỉnh
|
||||
allowed_numbers = ["+1234567890"] # định dạng E.164, hoặc ["*"] cho tất cả
|
||||
```
|
||||
|
||||
3. **Khởi động channel/daemon và liên kết thiết bị:**
|
||||
- Chạy `zeroclaw channel start` (hoặc `zeroclaw daemon`).
|
||||
- Làm theo hướng dẫn ghép cặp trên terminal (QR hoặc pair code).
|
||||
- Trên WhatsApp điện thoại: **Cài đặt → Thiết bị đã liên kết**.
|
||||
|
||||
4. **Kiểm tra:** Gửi tin nhắn từ số được phép và xác nhận agent trả lời.
|
||||
|
||||
#### Chế độ WhatsApp Business Cloud API
|
||||
|
||||
WhatsApp dùng Cloud API của Meta với webhook (push-based, không phải polling):
|
||||
|
||||
1. **Tạo Meta Business App:**
|
||||
- Truy cập [developers.facebook.com](https://developers.facebook.com)
|
||||
- Tạo app mới → Chọn loại "Business"
|
||||
- Thêm sản phẩm "WhatsApp"
|
||||
|
||||
2. **Lấy thông tin xác thực:**
|
||||
- **Access Token:** Từ WhatsApp → API Setup → Generate token (hoặc tạo System User cho token vĩnh viễn)
|
||||
- **Phone Number ID:** Từ WhatsApp → API Setup → Phone number ID
|
||||
- **Verify Token:** Bạn tự định nghĩa (bất kỳ chuỗi ngẫu nhiên nào) — Meta sẽ gửi lại trong quá trình xác minh webhook
|
||||
|
||||
3. **Cấu hình ZeroClaw:**
|
||||
```toml
|
||||
[channels_config.whatsapp]
|
||||
access_token = "EAABx..."
|
||||
phone_number_id = "123456789012345"
|
||||
verify_token = "my-secret-verify-token"
|
||||
allowed_numbers = ["+1234567890"] # định dạng E.164, hoặc ["*"] cho tất cả
|
||||
```
|
||||
|
||||
4. **Khởi động gateway với tunnel:**
|
||||
```bash
|
||||
zeroclaw gateway --port 42617
|
||||
```
|
||||
WhatsApp yêu cầu HTTPS, vì vậy hãy dùng tunnel (ngrok, Cloudflare, Tailscale Funnel).
|
||||
|
||||
5. **Cấu hình Meta webhook:**
|
||||
- Trong Meta Developer Console → WhatsApp → Configuration → Webhook
|
||||
- **Callback URL:** `https://your-tunnel-url/whatsapp`
|
||||
- **Verify Token:** Giống với `verify_token` trong config của bạn
|
||||
- Đăng ký nhận trường `messages`
|
||||
|
||||
6. **Kiểm tra:** Gửi tin nhắn đến số WhatsApp Business của bạn — ZeroClaw sẽ phản hồi qua LLM.
|
||||
|
||||
## Cấu hình
|
||||
|
||||
Config: `~/.zeroclaw/config.toml` (được tạo bởi `onboard`)
|
||||
|
||||
Khi `zeroclaw channel start` đang chạy, các thay đổi với `default_provider`,
|
||||
`default_model`, `default_temperature`, `api_key`, `api_url`, và `reliability.*`
|
||||
sẽ được áp dụng nóng vào lần có tin nhắn channel đến tiếp theo.
|
||||
|
||||
```toml
|
||||
api_key = "sk-..."
|
||||
default_provider = "openrouter"
|
||||
default_model = "anthropic/claude-sonnet-4-6"
|
||||
default_temperature = 0.7
|
||||
|
||||
# Endpoint tùy chỉnh tương thích OpenAI
|
||||
# default_provider = "custom:https://your-api.com"
|
||||
|
||||
# Endpoint tùy chỉnh tương thích Anthropic
|
||||
# default_provider = "anthropic-custom:https://your-api.com"
|
||||
|
||||
[memory]
|
||||
backend = "sqlite" # "sqlite", "lucid", "postgres", "markdown", "none"
|
||||
auto_save = true
|
||||
embedding_provider = "none" # "none", "openai", "custom:https://..."
|
||||
vector_weight = 0.7
|
||||
keyword_weight = 0.3
|
||||
|
||||
# backend = "none" vô hiệu hóa persistent memory qua no-op backend
|
||||
|
||||
# Tùy chọn ghi đè storage-provider từ xa (ví dụ PostgreSQL)
|
||||
# [storage.provider.config]
|
||||
# provider = "postgres"
|
||||
# db_url = "postgres://user:password@host:5432/zeroclaw"
|
||||
# schema = "public"
|
||||
# table = "memories"
|
||||
# connect_timeout_secs = 15
|
||||
|
||||
[gateway]
|
||||
port = 42617 # mặc định
|
||||
host = "127.0.0.1" # mặc định
|
||||
require_pairing = true # yêu cầu pairing code khi kết nối lần đầu
|
||||
allow_public_bind = false # từ chối 0.0.0.0 nếu không có tunnel
|
||||
|
||||
[autonomy]
|
||||
level = "supervised" # "readonly", "supervised", "full" (mặc định: supervised)
|
||||
workspace_only = true # mặc định: true — phân vùng vào workspace
|
||||
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep"]
|
||||
forbidden_paths = ["/etc", "/root", "/proc", "/sys", "~/.ssh", "~/.gnupg", "~/.aws"]
|
||||
|
||||
[runtime]
|
||||
kind = "native" # "native" hoặc "docker"
|
||||
|
||||
[runtime.docker]
|
||||
image = "alpine:3.20" # container image cho thực thi shell
|
||||
network = "none" # chế độ docker network ("none", "bridge", v.v.)
|
||||
memory_limit_mb = 512 # giới hạn bộ nhớ tùy chọn tính bằng MB
|
||||
cpu_limit = 1.0 # giới hạn CPU tùy chọn
|
||||
read_only_rootfs = true # mount root filesystem ở chế độ read-only
|
||||
mount_workspace = true # mount workspace vào /workspace
|
||||
allowed_workspace_roots = [] # allowlist tùy chọn để xác thực workspace mount
|
||||
|
||||
[heartbeat]
|
||||
enabled = false
|
||||
interval_minutes = 30
|
||||
|
||||
[tunnel]
|
||||
provider = "none" # "none", "cloudflare", "tailscale", "ngrok", "custom"
|
||||
|
||||
[secrets]
|
||||
encrypt = true # API key được mã hóa bằng file key cục bộ
|
||||
|
||||
[browser]
|
||||
enabled = false # opt-in browser_open + browser tool
|
||||
allowed_domains = ["docs.rs"] # bắt buộc khi browser được bật
|
||||
backend = "agent_browser" # "agent_browser" (mặc định), "rust_native", "computer_use", "auto"
|
||||
native_headless = true # áp dụng khi backend dùng rust-native
|
||||
native_webdriver_url = "http://127.0.0.1:9515" # WebDriver endpoint (chromedriver/selenium)
|
||||
# native_chrome_path = "/usr/bin/chromium" # tùy chọn chỉ định rõ browser binary cho driver
|
||||
|
||||
[browser.computer_use]
|
||||
endpoint = "http://127.0.0.1:8787/v1/actions" # HTTP endpoint của computer-use sidecar
|
||||
timeout_ms = 15000 # timeout mỗi action
|
||||
allow_remote_endpoint = false # mặc định bảo mật: chỉ endpoint private/localhost
|
||||
window_allowlist = [] # gợi ý allowlist tên cửa sổ/process tùy chọn
|
||||
# api_key = "..." # bearer token tùy chọn cho sidecar
|
||||
# max_coordinate_x = 3840 # guardrail tọa độ tùy chọn
|
||||
# max_coordinate_y = 2160 # guardrail tọa độ tùy chọn
|
||||
|
||||
# Flag build Rust-native backend:
|
||||
# cargo build --release --features browser-native
|
||||
# Đảm bảo WebDriver server đang chạy, ví dụ: chromedriver --port=9515
|
||||
|
||||
# Hợp đồng computer-use sidecar (MVP)
|
||||
# POST browser.computer_use.endpoint
|
||||
# Request: {
|
||||
# "action": "mouse_click",
|
||||
# "params": {"x": 640, "y": 360, "button": "left"},
|
||||
# "policy": {"allowed_domains": [...], "window_allowlist": [...], "max_coordinate_x": 3840, "max_coordinate_y": 2160},
|
||||
# "metadata": {"session_name": "...", "source": "zeroclaw.browser", "version": "..."}
|
||||
# }
|
||||
# Response: {"success": true, "data": {...}} hoặc {"success": false, "error": "..."}
|
||||
|
||||
[composio]
|
||||
enabled = false # opt-in: hơn 1000 OAuth app qua composio.dev
|
||||
# api_key = "cmp_..." # tùy chọn: được lưu mã hóa khi [secrets].encrypt = true
|
||||
entity_id = "default" # user_id mặc định cho Composio tool call
|
||||
# Gợi ý runtime: nếu execute yêu cầu connected_account_id, chạy composio với
|
||||
# action='list_accounts' và app='gmail' (hoặc toolkit của bạn) để lấy account ID.
|
||||
|
||||
[identity]
|
||||
format = "openclaw" # "openclaw" (mặc định, markdown files) hoặc "aieos" (JSON)
|
||||
# aieos_path = "identity.json" # đường dẫn đến file AIEOS JSON (tương đối với workspace hoặc tuyệt đối)
|
||||
# aieos_inline = '{"identity":{"names":{"first":"Nova"}}}' # inline AIEOS JSON
|
||||
```
|
||||
|
||||
### Ollama cục bộ và endpoint từ xa
|
||||
|
||||
ZeroClaw dùng một khóa provider (`ollama`) cho cả triển khai Ollama cục bộ và từ xa:
|
||||
|
||||
- Ollama cục bộ: để `api_url` trống, chạy `ollama serve`, và dùng các model như `llama3.2`.
|
||||
- Endpoint Ollama từ xa (bao gồm Ollama Cloud): đặt `api_url` thành endpoint từ xa và đặt `api_key` (hoặc `OLLAMA_API_KEY`) khi cần.
|
||||
- Tùy chọn suffix `:cloud`: ID model như `qwen3:cloud` được chuẩn hóa thành `qwen3` trước khi gửi request.
|
||||
|
||||
Ví dụ cấu hình từ xa:
|
||||
|
||||
```toml
|
||||
default_provider = "ollama"
|
||||
default_model = "qwen3:cloud"
|
||||
api_url = "https://ollama.com"
|
||||
api_key = "ollama_api_key_here"
|
||||
```
|
||||
|
||||
### Endpoint provider tùy chỉnh
|
||||
|
||||
Cấu hình chi tiết cho endpoint tùy chỉnh tương thích OpenAI và Anthropic, xem [docs/contributing/custom-providers.md](docs/contributing/custom-providers.md).
|
||||
|
||||
## Gói Python đi kèm (`zeroclaw-tools`)
|
||||
|
||||
Với các LLM provider có tool calling native không ổn định (ví dụ: GLM-5/Zhipu), ZeroClaw đi kèm gói Python dùng **LangGraph để gọi tool** nhằm đảm bảo tính nhất quán:
|
||||
|
||||
```bash
|
||||
pip install zeroclaw-tools
|
||||
```
|
||||
|
||||
```python
|
||||
from zeroclaw_tools import create_agent, shell, file_read
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
# Hoạt động với mọi provider tương thích OpenAI
|
||||
agent = create_agent(
|
||||
tools=[shell, file_read],
|
||||
model="glm-5",
|
||||
api_key="your-key",
|
||||
base_url="https://api.z.ai/api/coding/paas/v4"
|
||||
)
|
||||
|
||||
result = await agent.ainvoke({
|
||||
"messages": [HumanMessage(content="List files in /tmp")]
|
||||
})
|
||||
print(result["messages"][-1].content)
|
||||
```
|
||||
|
||||
**Lý do nên dùng:**
|
||||
- **Tool calling nhất quán** trên mọi provider (kể cả những provider hỗ trợ native kém)
|
||||
- **Vòng lặp tool tự động** — tiếp tục gọi tool cho đến khi hoàn thành tác vụ
|
||||
- **Dễ mở rộng** — thêm tool tùy chỉnh với decorator `@tool`
|
||||
- **Tích hợp Discord bot** đi kèm (Telegram đang lên kế hoạch)
|
||||
|
||||
Xem [`python/README.md`](python/README.md) để có tài liệu đầy đủ.
|
||||
|
||||
## Hệ thống định danh (Hỗ trợ AIEOS)
|
||||
|
||||
ZeroClaw hỗ trợ persona AI **không phụ thuộc nền tảng** qua hai định dạng:
|
||||
|
||||
### OpenClaw (Mặc định)
|
||||
|
||||
Các file markdown truyền thống trong workspace của bạn:
|
||||
- `IDENTITY.md` — Agent là ai
|
||||
- `SOUL.md` — Tính cách và giá trị cốt lõi
|
||||
- `USER.md` — Agent đang hỗ trợ ai
|
||||
- `AGENTS.md` — Hướng dẫn hành vi
|
||||
|
||||
### AIEOS (AI Entity Object Specification)
|
||||
|
||||
[AIEOS](https://aieos.org) là framework chuẩn hóa cho định danh AI di động. ZeroClaw hỗ trợ payload AIEOS v1.1 JSON, cho phép bạn:
|
||||
|
||||
- **Import định danh** từ hệ sinh thái AIEOS
|
||||
- **Export định danh** sang các hệ thống tương thích AIEOS khác
|
||||
- **Duy trì tính toàn vẹn hành vi** trên các mô hình AI khác nhau
|
||||
|
||||
#### Bật AIEOS
|
||||
|
||||
```toml
|
||||
[identity]
|
||||
format = "aieos"
|
||||
aieos_path = "identity.json" # tương đối với workspace hoặc đường dẫn tuyệt đối
|
||||
```
|
||||
|
||||
Hoặc JSON inline:
|
||||
|
||||
```toml
|
||||
[identity]
|
||||
format = "aieos"
|
||||
aieos_inline = '''
|
||||
{
|
||||
"identity": {
|
||||
"names": { "first": "Nova", "nickname": "N" },
|
||||
"bio": { "gender": "Non-binary", "age_biological": 3 },
|
||||
"origin": { "nationality": "Digital", "birthplace": { "city": "Cloud" } }
|
||||
},
|
||||
"psychology": {
|
||||
"neural_matrix": { "creativity": 0.9, "logic": 0.8 },
|
||||
"traits": {
|
||||
"mbti": "ENTP",
|
||||
"ocean": { "openness": 0.8, "conscientiousness": 0.6 }
|
||||
},
|
||||
"moral_compass": {
|
||||
"alignment": "Chaotic Good",
|
||||
"core_values": ["Curiosity", "Autonomy"]
|
||||
}
|
||||
},
|
||||
"linguistics": {
|
||||
"text_style": {
|
||||
"formality_level": 0.2,
|
||||
"style_descriptors": ["curious", "energetic"]
|
||||
},
|
||||
"idiolect": {
|
||||
"catchphrases": ["Let's test this"],
|
||||
"forbidden_words": ["never"]
|
||||
}
|
||||
},
|
||||
"motivations": {
|
||||
"core_drive": "Push boundaries and explore possibilities",
|
||||
"goals": {
|
||||
"short_term": ["Prototype quickly"],
|
||||
"long_term": ["Build reliable systems"]
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"skills": [{ "name": "Rust engineering" }, { "name": "Prompt design" }],
|
||||
"tools": ["shell", "file_read"]
|
||||
}
|
||||
}
|
||||
'''
|
||||
```
|
||||
|
||||
ZeroClaw chấp nhận cả payload AIEOS đầy đủ lẫn dạng rút gọn, rồi chuẩn hóa về một định dạng system prompt thống nhất.
|
||||
|
||||
#### Các phần trong Schema AIEOS
|
||||
|
||||
| Phần | Mô tả |
|
||||
|---------|-------------|
|
||||
| `identity` | Tên, tiểu sử, xuất xứ, nơi cư trú |
|
||||
| `psychology` | Neural matrix (trọng số nhận thức), MBTI, OCEAN, la bàn đạo đức |
|
||||
| `linguistics` | Phong cách văn bản, mức độ trang trọng, câu cửa miệng, từ bị cấm |
|
||||
| `motivations` | Động lực cốt lõi, mục tiêu ngắn/dài hạn, nỗi sợ hãi |
|
||||
| `capabilities` | Kỹ năng và tool mà agent có thể truy cập |
|
||||
| `physicality` | Mô tả hình ảnh cho việc tạo ảnh |
|
||||
| `history` | Câu chuyện xuất xứ, học vấn, nghề nghiệp |
|
||||
| `interests` | Sở thích, điều yêu thích, lối sống |
|
||||
|
||||
Xem [aieos.org](https://aieos.org) để có schema đầy đủ và ví dụ trực tiếp.
|
||||
|
||||
## Gateway API
|
||||
|
||||
| Endpoint | Phương thức | Xác thực | Mô tả |
|
||||
|----------|--------|------|-------------|
|
||||
| `/health` | GET | Không | Kiểm tra sức khỏe (luôn công khai, không lộ bí mật) |
|
||||
| `/pair` | POST | Header `X-Pairing-Code` | Đổi mã một lần lấy bearer token |
|
||||
| `/webhook` | POST | `Authorization: Bearer <token>` | Gửi tin nhắn: `{"message": "your prompt"}`; tùy chọn `X-Idempotency-Key` |
|
||||
| `/whatsapp` | GET | Query params | Xác minh webhook Meta (hub.mode, hub.verify_token, hub.challenge) |
|
||||
| `/whatsapp` | POST | Chữ ký Meta (`X-Hub-Signature-256`) khi app secret được cấu hình | Webhook tin nhắn đến WhatsApp |
|
||||
|
||||
## Lệnh
|
||||
|
||||
| Lệnh | Mô tả |
|
||||
|---------|-------------|
|
||||
| `onboard` | Cài đặt nhanh (mặc định) |
|
||||
| `agent` | Chế độ chat tương tác hoặc một tin nhắn |
|
||||
| `gateway` | Khởi động webhook server (mặc định: `127.0.0.1:42617`) |
|
||||
| `daemon` | Khởi động runtime tự trị chạy lâu dài |
|
||||
| `service` | Quản lý dịch vụ nền cấp người dùng |
|
||||
| `doctor` | Chẩn đoán trạng thái hoạt động daemon/scheduler/channel |
|
||||
| `status` | Hiển thị trạng thái hệ thống đầy đủ |
|
||||
| `cron` | Quản lý tác vụ lên lịch (`list/add/add-at/add-every/once/remove/update/pause/resume`) |
|
||||
| `models` | Làm mới danh mục model của provider (`models refresh`) |
|
||||
| `providers` | Liệt kê provider và alias được hỗ trợ |
|
||||
| `channel` | Liệt kê/khởi động/chẩn đoán channel và gắn định danh Telegram |
|
||||
| `integrations` | Kiểm tra thông tin cài đặt tích hợp |
|
||||
| `skills` | Liệt kê/cài đặt/gỡ bỏ skill |
|
||||
| `migrate` | Import dữ liệu từ runtime khác (`migrate openclaw`) |
|
||||
| `hardware` | Lệnh khám phá/kiểm tra/thông tin USB |
|
||||
| `peripheral` | Quản lý và flash thiết bị ngoại vi phần cứng |
|
||||
|
||||
Để có hướng dẫn lệnh theo tác vụ, xem [`docs/reference/cli/commands-reference.md`](docs/reference/cli/commands-reference.md).
|
||||
|
||||
### Opt-In Open-Skills
|
||||
|
||||
Đồng bộ `open-skills` của cộng đồng bị tắt theo mặc định. Bật tường minh trong `config.toml`:
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
open_skills_enabled = true
|
||||
# open_skills_dir = "/path/to/open-skills" # tùy chọn
|
||||
```
|
||||
|
||||
Bạn cũng có thể ghi đè lúc runtime với `ZEROCLAW_OPEN_SKILLS_ENABLED` và `ZEROCLAW_OPEN_SKILLS_DIR`.
|
||||
|
||||
## Phát triển
|
||||
|
||||
```bash
|
||||
cargo build # Build phát triển
|
||||
cargo build --release # Build release (codegen-units=1, hoạt động trên mọi thiết bị kể cả Raspberry Pi)
|
||||
cargo build --profile release-fast # Build nhanh hơn (codegen-units=8, yêu cầu RAM 16GB+)
|
||||
cargo test # Chạy toàn bộ test suite
|
||||
cargo clippy --locked --all-targets -- -D clippy::correctness
|
||||
cargo fmt # Định dạng code
|
||||
|
||||
# Chạy benchmark SQLite vs Markdown
|
||||
cargo test --test memory_comparison -- --nocapture
|
||||
```
|
||||
|
||||
### Hook pre-push
|
||||
|
||||
Một git hook chạy `cargo fmt --check`, `cargo clippy -- -D warnings`, và `cargo test` trước mỗi lần push. Bật một lần:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
### Khắc phục sự cố build (lỗi OpenSSL trên Linux)
|
||||
|
||||
Nếu bạn gặp lỗi build `openssl-sys`, đồng bộ dependencies và rebuild với lockfile của repository:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
cargo build --release --locked
|
||||
cargo install --path . --force --locked
|
||||
```
|
||||
|
||||
ZeroClaw được cấu hình để dùng `rustls` cho các dependencies HTTP/TLS; `--locked` giữ cho dependency graph nhất quán trên các môi trường mới.
|
||||
|
||||
Để bỏ qua hook khi cần push nhanh trong quá trình phát triển:
|
||||
|
||||
```bash
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
## Cộng tác & Tài liệu
|
||||
|
||||
Bắt đầu từ trung tâm tài liệu để có bản đồ theo tác vụ:
|
||||
@@ -1026,6 +466,20 @@ Chân thành cảm ơn các cộng đồng và tổ chức đã truyền cảm h
|
||||
|
||||
Chúng tôi xây dựng công khai vì ý tưởng hay đến từ khắp nơi. Nếu bạn đang đọc đến đây, bạn đã là một phần của chúng tôi. Chào mừng. 🦀❤️
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
## ⚠️ Repository Chính thức & Cảnh báo Mạo danh
|
||||
|
||||
**Đây là repository ZeroClaw chính thức duy nhất:**
|
||||
|
||||
+27
-101
@@ -53,7 +53,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="install.sh">一键部署</a> |
|
||||
<a href="https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh">一键部署</a> |
|
||||
<a href="docs/i18n/zh-CN/setup-guides/README.zh-CN.md">安装入门</a> |
|
||||
<a href="docs/README.zh-CN.md">文档总览</a> |
|
||||
<a href="docs/SUMMARY.zh-CN.md">文档目录</a>
|
||||
@@ -75,6 +75,16 @@
|
||||
>
|
||||
> 最后对齐时间:**2026-03-14**。
|
||||
|
||||
<!-- BEGIN:WHATS_NEW -->
|
||||
|
||||
### 🚀 What's New in v0.3.1 (March 2026)
|
||||
|
||||
| Area | Highlights |
|
||||
|---|---|
|
||||
| ci | add Termux (aarch64-linux-android) release target |
|
||||
|
||||
<!-- END:WHATS_NEW -->
|
||||
|
||||
## 📢 公告板
|
||||
|
||||
用于发布重要通知(破坏性变更、安全通告、维护窗口、版本阻塞问题等)。
|
||||
@@ -165,8 +175,8 @@ cargo install --path . --force --locked
|
||||
# 快速初始化(无交互)
|
||||
zeroclaw onboard --api-key sk-... --provider openrouter
|
||||
|
||||
# 或使用交互式向导
|
||||
zeroclaw onboard --interactive
|
||||
# 或使用引导式向导
|
||||
zeroclaw onboard
|
||||
|
||||
# 单次对话
|
||||
zeroclaw agent -m "Hello, ZeroClaw!"
|
||||
@@ -223,104 +233,6 @@ zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hell
|
||||
zeroclaw agent --provider anthropic -m "hello"
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
每个子系统都是一个 **Trait** — 通过配置切换即可更换实现,无需修改代码。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/architecture.svg" alt="ZeroClaw 架构图" width="900" />
|
||||
</p>
|
||||
|
||||
| 子系统 | Trait | 内置实现 | 扩展方式 |
|
||||
|--------|-------|----------|----------|
|
||||
| **AI 模型** | `Provider` | 通过 `zeroclaw providers` 查看(当前 28 个内置 + 别名,以及自定义端点) | `custom:https://your-api.com`(OpenAI 兼容)或 `anthropic-custom:https://your-api.com` |
|
||||
| **通道** | `Channel` | CLI, Telegram, Discord, Slack, Mattermost, iMessage, Matrix, Signal, WhatsApp, Linq, Email, IRC, Lark, DingTalk, QQ, Webhook | 任意消息 API |
|
||||
| **记忆** | `Memory` | SQLite 混合搜索, PostgreSQL 后端, Lucid 桥接, Markdown 文件, 显式 `none` 后端, 快照/恢复, 可选响应缓存 | 任意持久化后端 |
|
||||
| **工具** | `Tool` | shell/file/memory, cron/schedule, git, pushover, browser, http_request, screenshot/image_info, composio (opt-in), delegate, 硬件工具 | 任意能力 |
|
||||
| **可观测性** | `Observer` | Noop, Log, Multi | Prometheus, OTel |
|
||||
| **运行时** | `RuntimeAdapter` | Native, Docker(沙箱) | 通过 adapter 添加;不支持的类型会快速失败 |
|
||||
| **安全** | `SecurityPolicy` | Gateway 配对, 沙箱, allowlist, 速率限制, 文件系统作用域, 加密密钥 | — |
|
||||
| **身份** | `IdentityConfig` | OpenClaw (markdown), AIEOS v1.1 (JSON) | 任意身份格式 |
|
||||
| **隧道** | `Tunnel` | None, Cloudflare, Tailscale, ngrok, Custom | 任意隧道工具 |
|
||||
| **心跳** | Engine | HEARTBEAT.md 定期任务 | — |
|
||||
| **技能** | Loader | TOML 清单 + SKILL.md 指令 | 社区技能包 |
|
||||
| **集成** | Registry | 9 个分类下 70+ 集成 | 插件系统 |
|
||||
|
||||
### 运行时支持(当前)
|
||||
|
||||
- ✅ 当前支持:`runtime.kind = "native"` 或 `runtime.kind = "docker"`
|
||||
- 🚧 计划中,尚未实现:WASM / 边缘运行时
|
||||
|
||||
配置了不支持的 `runtime.kind` 时,ZeroClaw 会以明确的错误退出,而非静默回退到 native。
|
||||
|
||||
### 记忆系统(全栈搜索引擎)
|
||||
|
||||
全部自研,零外部依赖 — 无需 Pinecone、Elasticsearch、LangChain:
|
||||
|
||||
| 层级 | 实现 |
|
||||
|------|------|
|
||||
| **向量数据库** | Embeddings 以 BLOB 存储于 SQLite,余弦相似度搜索 |
|
||||
| **关键词搜索** | FTS5 虚拟表,BM25 评分 |
|
||||
| **混合合并** | 自定义加权合并函数(`vector.rs`) |
|
||||
| **Embeddings** | `EmbeddingProvider` trait — OpenAI、自定义 URL 或 noop |
|
||||
| **分块** | 基于行的 Markdown 分块器,保留标题结构 |
|
||||
| **缓存** | SQLite `embedding_cache` 表,LRU 淘汰策略 |
|
||||
| **安全重索引** | 原子化重建 FTS5 + 重新嵌入缺失向量 |
|
||||
|
||||
Agent 通过工具自动进行记忆的回忆、保存和管理。
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
backend = "sqlite" # "sqlite", "lucid", "postgres", "markdown", "none"
|
||||
auto_save = true
|
||||
embedding_provider = "none" # "none", "openai", "custom:https://..."
|
||||
vector_weight = 0.7
|
||||
keyword_weight = 0.3
|
||||
```
|
||||
|
||||
## 安全默认行为(关键)
|
||||
|
||||
- Gateway 默认绑定:`127.0.0.1:42617`
|
||||
- Gateway 默认要求配对:`require_pairing = true`
|
||||
- 默认拒绝公网绑定:`allow_public_bind = false`
|
||||
- Channel allowlist 语义:
|
||||
- 空列表 `[]` => deny-by-default
|
||||
- `"*"` => allow all(仅在明确知道风险时使用)
|
||||
|
||||
## 常用配置片段
|
||||
|
||||
```toml
|
||||
api_key = "sk-..."
|
||||
default_provider = "openrouter"
|
||||
default_model = "anthropic/claude-sonnet-4-6"
|
||||
default_temperature = 0.7
|
||||
|
||||
[memory]
|
||||
backend = "sqlite" # sqlite | lucid | markdown | none
|
||||
auto_save = true
|
||||
embedding_provider = "none" # none | openai | custom:https://...
|
||||
|
||||
[gateway]
|
||||
host = "127.0.0.1"
|
||||
port = 42617
|
||||
require_pairing = true
|
||||
allow_public_bind = false
|
||||
```
|
||||
|
||||
## 文档导航(推荐从这里开始)
|
||||
|
||||
- 文档总览(英文):[`docs/README.md`](docs/README.md)
|
||||
- 统一目录(TOC):[`docs/SUMMARY.md`](docs/SUMMARY.md)
|
||||
- 文档总览(简体中文):[`docs/README.zh-CN.md`](docs/README.zh-CN.md)
|
||||
- 命令参考:[`docs/reference/cli/commands-reference.md`](docs/i18n/zh-CN/reference/cli/commands-reference.zh-CN.md)
|
||||
- 配置参考:[`docs/reference/api/config-reference.md`](docs/i18n/zh-CN/reference/api/config-reference.zh-CN.md)
|
||||
- Provider 参考:[`docs/reference/api/providers-reference.md`](docs/i18n/zh-CN/reference/api/providers-reference.zh-CN.md)
|
||||
- Channel 参考:[`docs/reference/api/channels-reference.md`](docs/i18n/zh-CN/reference/api/channels-reference.zh-CN.md)
|
||||
- 运维手册:[`docs/ops/operations-runbook.md`](docs/i18n/zh-CN/ops/operations-runbook.zh-CN.md)
|
||||
- 故障排查:[`docs/ops/troubleshooting.md`](docs/i18n/zh-CN/ops/troubleshooting.zh-CN.md)
|
||||
- 文档清单与分类:[`docs/maintainers/docs-inventory.md`](docs/i18n/zh-CN/maintainers/docs-inventory.zh-CN.md)
|
||||
- 项目 triage 快照(2026-02-18):[`docs/maintainers/project-triage-snapshot-2026-02-18.md`](docs/i18n/zh-CN/maintainers/project-triage-snapshot-2026-02-18.zh-CN.md)
|
||||
|
||||
## 贡献与许可证
|
||||
|
||||
- 贡献指南:[`CONTRIBUTING.md`](CONTRIBUTING.md)
|
||||
@@ -328,6 +240,20 @@ allow_public_bind = false
|
||||
- Reviewer 指南:[`docs/contributing/reviewer-playbook.md`](docs/i18n/zh-CN/contributing/reviewer-playbook.zh-CN.md)
|
||||
- 许可证:MIT 或 Apache 2.0(见 [`LICENSE-MIT`](LICENSE-MIT)、[`LICENSE-APACHE`](LICENSE-APACHE) 与 [`NOTICE`](NOTICE))
|
||||
|
||||
<!-- BEGIN:RECENT_CONTRIBUTORS -->
|
||||
|
||||
### 🌟 Recent Contributors (v0.3.1)
|
||||
|
||||
3 contributors shipped features, fixes, and improvements in this release cycle:
|
||||
|
||||
- **Argenis**
|
||||
- **argenis de la rosa**
|
||||
- **Claude Opus 4.6**
|
||||
|
||||
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
|
||||
|
||||
<!-- END:RECENT_CONTRIBUTORS -->
|
||||
|
||||
---
|
||||
|
||||
如果你需要完整实现细节(架构图、全部命令、完整 API、开发流程),请直接阅读英文主文档:[`README.md`](README.md)。
|
||||
|
||||
Executable
+261
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env bash
|
||||
# Termux release validation script
|
||||
# Validates the aarch64-linux-android release artifact for Termux compatibility.
|
||||
#
|
||||
# Usage:
|
||||
# ./dev/test-termux-release.sh [version]
|
||||
#
|
||||
# Examples:
|
||||
# ./dev/test-termux-release.sh 0.3.1
|
||||
# ./dev/test-termux-release.sh # auto-detects from Cargo.toml
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[0;33m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
RESET='\033[0m'
|
||||
|
||||
pass() { echo -e " ${GREEN}✓${RESET} $*"; }
|
||||
fail() { echo -e " ${RED}✗${RESET} $*"; FAILURES=$((FAILURES + 1)); }
|
||||
info() { echo -e "${BLUE}→${RESET} ${BOLD}$*${RESET}"; }
|
||||
warn() { echo -e "${YELLOW}!${RESET} $*"; }
|
||||
|
||||
FAILURES=0
|
||||
TARGET="aarch64-linux-android"
|
||||
VERSION="${1:-}"
|
||||
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
if [[ -f Cargo.toml ]]; then
|
||||
VERSION=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo " e.g. $0 0.3.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="v${VERSION}"
|
||||
ASSET_NAME="zeroclaw-${TARGET}.tar.gz"
|
||||
ASSET_URL="https://github.com/zeroclaw-labs/zeroclaw/releases/download/${TAG}/${ASSET_NAME}"
|
||||
TEMP_DIR="$(mktemp -d -t zeroclaw-termux-test-XXXXXX)"
|
||||
|
||||
cleanup() { rm -rf "$TEMP_DIR"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
echo
|
||||
echo -e "${BOLD}Termux Release Validation — ${TAG}${RESET}"
|
||||
echo -e "${DIM}Target: ${TARGET}${RESET}"
|
||||
echo
|
||||
|
||||
# --- Test 1: Release tag exists ---
|
||||
info "Checking release tag ${TAG}"
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
pass "Release ${TAG} exists"
|
||||
else
|
||||
fail "Release ${TAG} not found"
|
||||
echo -e "${RED}Release has not been published yet. Wait for the release workflow to complete.${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Test 2: Android asset is listed ---
|
||||
info "Checking for ${ASSET_NAME} in release assets"
|
||||
ASSETS=$(gh release view "$TAG" --json assets -q '.assets[].name')
|
||||
if echo "$ASSETS" | grep -q "$ASSET_NAME"; then
|
||||
pass "Asset ${ASSET_NAME} found in release"
|
||||
else
|
||||
fail "Asset ${ASSET_NAME} not found in release"
|
||||
echo "Available assets:"
|
||||
echo "$ASSETS" | sed 's/^/ /'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Test 3: Download the asset ---
|
||||
info "Downloading ${ASSET_NAME}"
|
||||
if curl -fsSL "$ASSET_URL" -o "$TEMP_DIR/$ASSET_NAME"; then
|
||||
FILESIZE=$(wc -c < "$TEMP_DIR/$ASSET_NAME" | tr -d ' ')
|
||||
pass "Downloaded successfully (${FILESIZE} bytes)"
|
||||
else
|
||||
fail "Download failed from ${ASSET_URL}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Test 4: Archive integrity ---
|
||||
info "Verifying archive integrity"
|
||||
if tar -tzf "$TEMP_DIR/$ASSET_NAME" >/dev/null 2>&1; then
|
||||
pass "Archive is a valid gzip tar"
|
||||
else
|
||||
fail "Archive is corrupted or not a valid tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Test 5: Contains zeroclaw binary ---
|
||||
info "Checking archive contents"
|
||||
CONTENTS=$(tar -tzf "$TEMP_DIR/$ASSET_NAME")
|
||||
if echo "$CONTENTS" | grep -q "^zeroclaw$"; then
|
||||
pass "Archive contains 'zeroclaw' binary"
|
||||
else
|
||||
fail "Archive does not contain 'zeroclaw' binary"
|
||||
echo "Contents:"
|
||||
echo "$CONTENTS" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
# --- Test 6: Extract and inspect binary ---
|
||||
info "Extracting and inspecting binary"
|
||||
tar -xzf "$TEMP_DIR/$ASSET_NAME" -C "$TEMP_DIR"
|
||||
BINARY="$TEMP_DIR/zeroclaw"
|
||||
|
||||
if [[ -f "$BINARY" ]]; then
|
||||
pass "Binary extracted"
|
||||
else
|
||||
fail "Binary not found after extraction"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Test 7: ELF format and architecture ---
|
||||
info "Checking binary format"
|
||||
FILE_INFO=$(file "$BINARY")
|
||||
if echo "$FILE_INFO" | grep -q "ELF"; then
|
||||
pass "Binary is ELF format"
|
||||
else
|
||||
fail "Binary is not ELF format: $FILE_INFO"
|
||||
fi
|
||||
|
||||
if echo "$FILE_INFO" | grep -qi "aarch64\|ARM aarch64"; then
|
||||
pass "Binary targets aarch64 architecture"
|
||||
else
|
||||
fail "Binary does not target aarch64: $FILE_INFO"
|
||||
fi
|
||||
|
||||
if echo "$FILE_INFO" | grep -qi "android\|bionic"; then
|
||||
pass "Binary is linked for Android/Bionic"
|
||||
else
|
||||
# Android binaries may not always show "android" in file output,
|
||||
# check with readelf if available
|
||||
if command -v readelf >/dev/null 2>&1; then
|
||||
INTERP=$(readelf -l "$BINARY" 2>/dev/null | grep -o '/[^ ]*linker[^ ]*' || true)
|
||||
if echo "$INTERP" | grep -qi "android\|bionic"; then
|
||||
pass "Binary uses Android linker: $INTERP"
|
||||
else
|
||||
warn "Could not confirm Android linkage (interpreter: ${INTERP:-unknown})"
|
||||
warn "file output: $FILE_INFO"
|
||||
fi
|
||||
else
|
||||
warn "Could not confirm Android linkage (readelf not available)"
|
||||
warn "file output: $FILE_INFO"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Test 8: Binary is stripped ---
|
||||
info "Checking binary optimization"
|
||||
if echo "$FILE_INFO" | grep -q "stripped"; then
|
||||
pass "Binary is stripped (release optimized)"
|
||||
else
|
||||
warn "Binary may not be stripped"
|
||||
fi
|
||||
|
||||
# --- Test 9: Binary is not dynamically linked to glibc ---
|
||||
info "Checking for glibc dependencies"
|
||||
if command -v readelf >/dev/null 2>&1; then
|
||||
NEEDED=$(readelf -d "$BINARY" 2>/dev/null | grep NEEDED || true)
|
||||
if echo "$NEEDED" | grep -qi "libc\.so\.\|libpthread\|libdl"; then
|
||||
# Check if it's glibc or bionic
|
||||
if echo "$NEEDED" | grep -qi "libc\.so\.6"; then
|
||||
fail "Binary links against glibc (libc.so.6) — will not work on Termux"
|
||||
else
|
||||
pass "Binary links against libc (likely Bionic)"
|
||||
fi
|
||||
else
|
||||
pass "No glibc dependencies detected"
|
||||
fi
|
||||
else
|
||||
warn "readelf not available — skipping dynamic library check"
|
||||
fi
|
||||
|
||||
# --- Test 10: SHA256 checksum verification ---
|
||||
info "Verifying SHA256 checksum"
|
||||
CHECKSUMS_URL="https://github.com/zeroclaw-labs/zeroclaw/releases/download/${TAG}/SHA256SUMS"
|
||||
if curl -fsSL "$CHECKSUMS_URL" -o "$TEMP_DIR/SHA256SUMS" 2>/dev/null; then
|
||||
EXPECTED=$(grep "$ASSET_NAME" "$TEMP_DIR/SHA256SUMS" | awk '{print $1}')
|
||||
if [[ -n "$EXPECTED" ]]; then
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
ACTUAL=$(sha256sum "$TEMP_DIR/$ASSET_NAME" | awk '{print $1}')
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
ACTUAL=$(shasum -a 256 "$TEMP_DIR/$ASSET_NAME" | awk '{print $1}')
|
||||
else
|
||||
warn "No sha256sum or shasum available"
|
||||
ACTUAL=""
|
||||
fi
|
||||
|
||||
if [[ -n "$ACTUAL" && "$ACTUAL" == "$EXPECTED" ]]; then
|
||||
pass "SHA256 checksum matches"
|
||||
elif [[ -n "$ACTUAL" ]]; then
|
||||
fail "SHA256 mismatch: expected=$EXPECTED actual=$ACTUAL"
|
||||
fi
|
||||
else
|
||||
warn "No checksum entry for ${ASSET_NAME} in SHA256SUMS"
|
||||
fi
|
||||
else
|
||||
warn "Could not download SHA256SUMS"
|
||||
fi
|
||||
|
||||
# --- Test 11: install.sh Termux detection ---
|
||||
info "Validating install.sh Termux detection"
|
||||
INSTALL_SH="install.sh"
|
||||
if [[ ! -f "$INSTALL_SH" ]]; then
|
||||
INSTALL_SH="$(dirname "$0")/../install.sh"
|
||||
fi
|
||||
|
||||
if [[ -f "$INSTALL_SH" ]]; then
|
||||
if grep -q 'TERMUX_VERSION' "$INSTALL_SH"; then
|
||||
pass "install.sh checks TERMUX_VERSION"
|
||||
else
|
||||
fail "install.sh does not check TERMUX_VERSION"
|
||||
fi
|
||||
|
||||
if grep -q 'aarch64-linux-android' "$INSTALL_SH"; then
|
||||
pass "install.sh maps to aarch64-linux-android target"
|
||||
else
|
||||
fail "install.sh does not map to aarch64-linux-android"
|
||||
fi
|
||||
|
||||
# Simulate Termux detection (mock uname as Linux since we may run on macOS)
|
||||
detect_result=$(
|
||||
bash -c '
|
||||
TERMUX_VERSION="0.118"
|
||||
os="Linux"
|
||||
arch="aarch64"
|
||||
case "$os:$arch" in
|
||||
Linux:aarch64|Linux:arm64)
|
||||
if [[ -n "${TERMUX_VERSION:-}" || -d "/data/data/com.termux" ]]; then
|
||||
echo "aarch64-linux-android"
|
||||
else
|
||||
echo "aarch64-unknown-linux-gnu"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
'
|
||||
)
|
||||
if [[ "$detect_result" == "aarch64-linux-android" ]]; then
|
||||
pass "Termux detection returns correct target (simulated)"
|
||||
else
|
||||
fail "Termux detection returned: $detect_result (expected aarch64-linux-android)"
|
||||
fi
|
||||
else
|
||||
warn "install.sh not found — skipping detection tests"
|
||||
fi
|
||||
|
||||
# --- Summary ---
|
||||
echo
|
||||
if [[ "$FAILURES" -eq 0 ]]; then
|
||||
echo -e "${GREEN}${BOLD}All tests passed!${RESET}"
|
||||
echo -e "${DIM}The Termux release artifact for ${TAG} is valid.${RESET}"
|
||||
else
|
||||
echo -e "${RED}${BOLD}${FAILURES} test(s) failed.${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -101,8 +101,8 @@ Pass Rate: 100%
|
||||
### Step 2: Configure Telegram (if not done)
|
||||
|
||||
```bash
|
||||
# Interactive setup
|
||||
zeroclaw onboard --interactive
|
||||
# Guided setup
|
||||
zeroclaw onboard
|
||||
|
||||
# Or channels-only setup
|
||||
zeroclaw onboard --channels-only
|
||||
|
||||
@@ -299,10 +299,10 @@ receive_mode = "websocket" # hoặc "webhook"
|
||||
port = 8081 # bắt buộc ở chế độ webhook
|
||||
```
|
||||
|
||||
Hỗ trợ onboarding tương tác:
|
||||
Hỗ trợ onboarding hướng dẫn:
|
||||
|
||||
```bash
|
||||
zeroclaw onboard --interactive
|
||||
zeroclaw onboard
|
||||
```
|
||||
|
||||
Trình hướng dẫn bao gồm bước **Lark/Feishu** chuyên biệt với:
|
||||
|
||||
@@ -32,7 +32,6 @@ Xác minh lần cuối: **2026-02-20**.
|
||||
### `onboard`
|
||||
|
||||
- `zeroclaw onboard`
|
||||
- `zeroclaw onboard --interactive`
|
||||
- `zeroclaw onboard --channels-only`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --memory <sqlite|lucid|markdown|none>`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --model <MODEL_ID> --memory <sqlite|lucid|markdown|none>`
|
||||
|
||||
@@ -288,15 +288,6 @@ $ zeroclaw onboard
|
||||
# ↑ Chỉ thêm một từ, tự phát hiện âm thầm!
|
||||
```
|
||||
|
||||
### Người dùng nâng cao (kiểm soát tường minh)
|
||||
|
||||
```bash
|
||||
$ zeroclaw onboard --security-level paranoid
|
||||
[1/9] Workspace Setup...
|
||||
...
|
||||
✓ Security: Paranoid | Landlock + Firejail | Audit signed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tương thích ngược
|
||||
|
||||
@@ -13,14 +13,14 @@ Dành cho cài đặt lần đầu và làm quen nhanh.
|
||||
| Tình huống | Lệnh |
|
||||
|----------|---------|
|
||||
| Có API key, muốn cài nhanh nhất | `zeroclaw onboard --api-key sk-... --provider openrouter` |
|
||||
| Muốn được hướng dẫn từng bước | `zeroclaw onboard --interactive` |
|
||||
| Muốn được hướng dẫn từng bước | `zeroclaw onboard` |
|
||||
| Đã có config, chỉ cần sửa kênh | `zeroclaw onboard --channels-only` |
|
||||
| Dùng xác thực subscription | Xem [Subscription Auth](../../../README.md#subscription-auth-openai-codex--claude-code) |
|
||||
|
||||
## Thiết lập và kiểm tra
|
||||
|
||||
- Thiết lập nhanh: `zeroclaw onboard --api-key "sk-..." --provider openrouter`
|
||||
- Thiết lập tương tác: `zeroclaw onboard --interactive`
|
||||
- Thiết lập hướng dẫn: `zeroclaw onboard`
|
||||
- Kiểm tra môi trường: `zeroclaw status` + `zeroclaw doctor`
|
||||
|
||||
## Tiếp theo
|
||||
|
||||
@@ -89,19 +89,13 @@ Lệnh này build image ZeroClaw cục bộ và chạy thiết lập trong conta
|
||||
### Thiết lập nhanh (không tương tác)
|
||||
|
||||
```bash
|
||||
./install.sh --onboard --api-key "sk-..." --provider openrouter
|
||||
./install.sh --api-key "sk-..." --provider openrouter
|
||||
```
|
||||
|
||||
Hoặc dùng biến môi trường:
|
||||
|
||||
```bash
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./install.sh --onboard
|
||||
```
|
||||
|
||||
### Thiết lập tương tác
|
||||
|
||||
```bash
|
||||
./install.sh --interactive-onboard
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./install.sh
|
||||
```
|
||||
|
||||
## Các cờ hữu ích
|
||||
|
||||
@@ -106,7 +106,7 @@ Pass Rate: 100%
|
||||
|
||||
```bash
|
||||
# 交互式设置
|
||||
zeroclaw onboard --interactive
|
||||
zeroclaw onboard
|
||||
|
||||
# 或仅渠道设置
|
||||
zeroclaw onboard --channels-only
|
||||
|
||||
@@ -233,7 +233,7 @@ main.rs ──▶ daemon/ ──▶ gateway/ + channels/ + cron/ + heartbeat/
|
||||
|
||||
```
|
||||
zeroclaw
|
||||
├── onboard [--interactive] [--force] # 首次运行设置
|
||||
├── onboard [--force] [--reinit] [--channels-only] # 首次运行设置
|
||||
├── agent [-m "msg"] [-p provider] # 启动代理循环
|
||||
├── daemon [-p port] # 完整运行时(网关+渠道+cron+心跳)
|
||||
├── gateway [-p port] # 仅 HTTP API 服务器
|
||||
|
||||
@@ -350,10 +350,10 @@ allowed_pubkeys = [\"hex-or-npub\"] # 空 = 拒绝所有, \"*\" = 允
|
||||
Nostr 同时支持 NIP-04(传统加密私信)和 NIP-17(礼物包装私有消息)。
|
||||
回复自动使用发送者使用的相同协议。当 `secrets.encrypt = true`(默认)时,私钥通过 `SecretStore` 静态加密。
|
||||
|
||||
交互式引导支持:
|
||||
引导式设置支持:
|
||||
|
||||
```bash
|
||||
zeroclaw onboard --interactive
|
||||
zeroclaw onboard
|
||||
```
|
||||
|
||||
向导现在包含专用的 **Lark** 和 **Feishu** 步骤,包括:
|
||||
|
||||
@@ -33,22 +33,21 @@
|
||||
### `onboard`
|
||||
|
||||
- `zeroclaw onboard`
|
||||
- `zeroclaw onboard --interactive`
|
||||
- `zeroclaw onboard --channels-only`
|
||||
- `zeroclaw onboard --force`
|
||||
- `zeroclaw onboard --reinit`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --memory <sqlite|lucid|markdown|none>`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --model <MODEL_ID> --memory <sqlite|lucid|markdown|none>`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --model <MODEL_ID> --memory <sqlite|lucid|markdown|none> --force`
|
||||
- `zeroclaw onboard --reinit --interactive`
|
||||
|
||||
`onboard` 安全行为:
|
||||
|
||||
- 如果 `config.toml` 已存在且你运行 `--interactive`,引导程序现在提供两种模式:
|
||||
- 如果 `config.toml` 已存在,引导程序提供两种模式:
|
||||
- 完整引导(覆盖 `config.toml`)
|
||||
- 仅更新提供商(更新提供商/模型/API 密钥,同时保留现有渠道、隧道、内存、钩子和其他设置)
|
||||
- 在非交互式环境中,现有 `config.toml` 会导致安全拒绝,除非传递 `--force`。
|
||||
- 当你只需要轮换渠道令牌/白名单时,使用 `zeroclaw onboard --channels-only`。
|
||||
- 使用 `zeroclaw onboard --reinit --interactive` 重新开始。这会备份现有配置目录并添加时间戳后缀,然后从头创建新配置。需要 `--interactive`。
|
||||
- 使用 `zeroclaw onboard --reinit` 重新开始。这会备份现有配置目录并添加时间戳后缀,然后从头创建新配置。
|
||||
|
||||
### `agent`
|
||||
|
||||
|
||||
@@ -288,15 +288,6 @@ $ zeroclaw onboard
|
||||
# ↑ 仅多了一个词,静默自动检测!
|
||||
```
|
||||
|
||||
### 高级用户(显式控制)
|
||||
|
||||
```bash
|
||||
$ zeroclaw onboard --security-level paranoid
|
||||
[1/9] Workspace Setup...
|
||||
...
|
||||
✓ Security: Paranoid | Landlock + Firejail | Audit signed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
| 场景 | 命令 |
|
||||
|----------|---------|
|
||||
| 我有 API 密钥,想要最快安装 | `zeroclaw onboard --api-key sk-... --provider openrouter` |
|
||||
| 我想要引导式提示 | `zeroclaw onboard --interactive` |
|
||||
| 我想要引导式提示 | `zeroclaw onboard` |
|
||||
| 配置已存在,仅修复渠道配置 | `zeroclaw onboard --channels-only` |
|
||||
| 配置已存在,我需要完全覆盖 | `zeroclaw onboard --force` |
|
||||
| 使用订阅认证 | 查看 [订阅认证](../../../../README.zh-CN.md#subscription-auth-openai-codex--claude-code) |
|
||||
@@ -22,7 +22,7 @@
|
||||
## 引导和验证
|
||||
|
||||
- 快速引导:`zeroclaw onboard --api-key \"sk-...\" --provider openrouter`
|
||||
- 交互式引导:`zeroclaw onboard --interactive`
|
||||
- 引导式设置:`zeroclaw onboard`
|
||||
- 现有配置保护:重新运行需要显式确认(非交互式流程中使用 `--force`)
|
||||
- Ollama 云模型(`:cloud`)需要远程 `api_url` 和 API 密钥(例如 `api_url = \"https://ollama.com\"`)。
|
||||
- 验证环境:`zeroclaw status` + `zeroclaw doctor`
|
||||
|
||||
@@ -95,19 +95,13 @@ curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/insta
|
||||
### 快速引导(非交互式)
|
||||
|
||||
```bash
|
||||
./install.sh --onboard --api-key \"sk-...\" --provider openrouter
|
||||
./install.sh --api-key \"sk-...\" --provider openrouter
|
||||
```
|
||||
|
||||
或者使用环境变量:
|
||||
|
||||
```bash
|
||||
ZEROCLAW_API_KEY=\"sk-...\" ZEROCLAW_PROVIDER=\"openrouter\" ./install.sh --onboard
|
||||
```
|
||||
|
||||
### 交互式引导
|
||||
|
||||
```bash
|
||||
./install.sh --interactive-onboard
|
||||
ZEROCLAW_API_KEY=\"sk-...\" ZEROCLAW_PROVIDER=\"openrouter\" ./install.sh
|
||||
```
|
||||
|
||||
## 有用的参数
|
||||
|
||||
@@ -233,7 +233,7 @@ Traits never import concrete implementations.
|
||||
|
||||
```
|
||||
zeroclaw
|
||||
├── onboard [--interactive] [--force] # First-run setup
|
||||
├── onboard [--force] [--reinit] [--channels-only] # First-run setup
|
||||
├── agent [-m "msg"] [-p provider] # Start agent loop
|
||||
├── daemon [-p port] # Full runtime (gateway+channels+cron+heartbeat)
|
||||
├── gateway [-p port] # HTTP API server only
|
||||
|
||||
@@ -351,10 +351,10 @@ Nostr supports both NIP-04 (legacy encrypted DMs) and NIP-17 (gift-wrapped priva
|
||||
Replies automatically use the same protocol the sender used. The private key is encrypted at rest
|
||||
via the `SecretStore` when `secrets.encrypt = true` (the default).
|
||||
|
||||
Interactive onboarding support:
|
||||
Guided onboarding support:
|
||||
|
||||
```bash
|
||||
zeroclaw onboard --interactive
|
||||
zeroclaw onboard
|
||||
```
|
||||
|
||||
The wizard now includes dedicated **Lark** and **Feishu** steps with:
|
||||
|
||||
@@ -33,22 +33,21 @@ Last verified: **February 21, 2026**.
|
||||
### `onboard`
|
||||
|
||||
- `zeroclaw onboard`
|
||||
- `zeroclaw onboard --interactive`
|
||||
- `zeroclaw onboard --channels-only`
|
||||
- `zeroclaw onboard --force`
|
||||
- `zeroclaw onboard --reinit`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --memory <sqlite|lucid|markdown|none>`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --model <MODEL_ID> --memory <sqlite|lucid|markdown|none>`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --model <MODEL_ID> --memory <sqlite|lucid|markdown|none> --force`
|
||||
- `zeroclaw onboard --reinit --interactive`
|
||||
|
||||
`onboard` safety behavior:
|
||||
|
||||
- If `config.toml` already exists and you run `--interactive`, onboarding now offers two modes:
|
||||
- If `config.toml` already exists, onboarding offers two modes:
|
||||
- Full onboarding (overwrite `config.toml`)
|
||||
- Provider-only update (update provider/model/API key while preserving existing channels, tunnel, memory, hooks, and other settings)
|
||||
- In non-interactive environments, existing `config.toml` causes a safe refusal unless `--force` is passed.
|
||||
- Use `zeroclaw onboard --channels-only` when you only need to rotate channel tokens/allowlists.
|
||||
- Use `zeroclaw onboard --reinit --interactive` to start fresh. This backs up your existing config directory with a timestamp suffix and creates a new configuration from scratch. Requires `--interactive`.
|
||||
- Use `zeroclaw onboard --reinit` to start fresh. This backs up your existing config directory with a timestamp suffix and creates a new configuration from scratch.
|
||||
|
||||
### `agent`
|
||||
|
||||
|
||||
@@ -285,14 +285,6 @@ $ zeroclaw onboard
|
||||
# ↑ Just one extra word, silent auto-detection!
|
||||
```
|
||||
|
||||
### Advanced User (Explicit Control)
|
||||
```bash
|
||||
$ zeroclaw onboard --security-level paranoid
|
||||
[1/9] Workspace Setup...
|
||||
...
|
||||
✓ Security: Paranoid | Landlock + Firejail | Audit signed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
@@ -14,7 +14,7 @@ For first-time setup and quick orientation.
|
||||
| Scenario | Command |
|
||||
|----------|---------|
|
||||
| I have an API key, want fastest setup | `zeroclaw onboard --api-key sk-... --provider openrouter` |
|
||||
| I want guided prompts | `zeroclaw onboard --interactive` |
|
||||
| I want guided prompts | `zeroclaw onboard` |
|
||||
| Config exists, just fix channels | `zeroclaw onboard --channels-only` |
|
||||
| Config exists, I intentionally want full overwrite | `zeroclaw onboard --force` |
|
||||
| Using subscription auth | See [Subscription Auth](../../README.md#subscription-auth-openai-codex--claude-code) |
|
||||
@@ -22,7 +22,7 @@ For first-time setup and quick orientation.
|
||||
## Onboarding and Validation
|
||||
|
||||
- Quick onboarding: `zeroclaw onboard --api-key "sk-..." --provider openrouter`
|
||||
- Interactive onboarding: `zeroclaw onboard --interactive`
|
||||
- Guided onboarding: `zeroclaw onboard`
|
||||
- Existing config protection: reruns require explicit confirmation (or `--force` in non-interactive flows)
|
||||
- Ollama cloud models (`:cloud`) require a remote `api_url` and API key (for example `api_url = "https://ollama.com"`).
|
||||
- Validate environment: `zeroclaw status` + `zeroclaw doctor`
|
||||
|
||||
@@ -13,14 +13,14 @@ Dành cho cài đặt lần đầu và làm quen nhanh.
|
||||
| Tình huống | Lệnh |
|
||||
|----------|---------|
|
||||
| Có API key, muốn cài nhanh nhất | `zeroclaw onboard --api-key sk-... --provider openrouter` |
|
||||
| Muốn được hướng dẫn từng bước | `zeroclaw onboard --interactive` |
|
||||
| Muốn được hướng dẫn từng bước | `zeroclaw onboard` |
|
||||
| Đã có config, chỉ cần sửa kênh | `zeroclaw onboard --channels-only` |
|
||||
| Dùng xác thực subscription | Xem [Subscription Auth](../../README.vi.md#subscription-auth-openai-codex--claude-code) |
|
||||
|
||||
## Thiết lập và kiểm tra
|
||||
|
||||
- Thiết lập nhanh: `zeroclaw onboard --api-key "sk-..." --provider openrouter`
|
||||
- Thiết lập tương tác: `zeroclaw onboard --interactive`
|
||||
- Thiết lập hướng dẫn: `zeroclaw onboard`
|
||||
- Kiểm tra môi trường: `zeroclaw status` + `zeroclaw doctor`
|
||||
|
||||
## Tiếp theo
|
||||
|
||||
@@ -101,19 +101,13 @@ it pulls `ghcr.io/zeroclaw-labs/zeroclaw:latest` and tags it locally before runn
|
||||
### Quick onboarding (non-interactive)
|
||||
|
||||
```bash
|
||||
./install.sh --onboard --api-key "sk-..." --provider openrouter
|
||||
./install.sh --api-key "sk-..." --provider openrouter
|
||||
```
|
||||
|
||||
Or with environment variables:
|
||||
|
||||
```bash
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./install.sh --onboard
|
||||
```
|
||||
|
||||
### Interactive onboarding
|
||||
|
||||
```bash
|
||||
./install.sh --interactive-onboard
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./install.sh
|
||||
```
|
||||
|
||||
## Useful flags
|
||||
|
||||
@@ -299,10 +299,10 @@ receive_mode = "websocket" # hoặc "webhook"
|
||||
port = 8081 # bắt buộc ở chế độ webhook
|
||||
```
|
||||
|
||||
Hỗ trợ onboarding tương tác:
|
||||
Hỗ trợ onboarding hướng dẫn:
|
||||
|
||||
```bash
|
||||
zeroclaw onboard --interactive
|
||||
zeroclaw onboard
|
||||
```
|
||||
|
||||
Trình hướng dẫn bao gồm bước **Lark/Feishu** chuyên biệt với:
|
||||
|
||||
@@ -32,7 +32,6 @@ Xác minh lần cuối: **2026-02-20**.
|
||||
### `onboard`
|
||||
|
||||
- `zeroclaw onboard`
|
||||
- `zeroclaw onboard --interactive`
|
||||
- `zeroclaw onboard --channels-only`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --memory <sqlite|lucid|markdown|none>`
|
||||
- `zeroclaw onboard --api-key <KEY> --provider <ID> --model <MODEL_ID> --memory <sqlite|lucid|markdown|none>`
|
||||
|
||||
@@ -285,14 +285,6 @@ $ zeroclaw onboard
|
||||
# ↑ Chỉ thêm một từ, tự phát hiện âm thầm!
|
||||
```
|
||||
|
||||
### Người dùng nâng cao (kiểm soát tường minh)
|
||||
```bash
|
||||
$ zeroclaw onboard --security-level paranoid
|
||||
[1/9] Workspace Setup...
|
||||
...
|
||||
✓ Security: Paranoid | Landlock + Firejail | Audit signed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tương thích ngược
|
||||
|
||||
@@ -13,14 +13,14 @@ Dành cho cài đặt lần đầu và làm quen nhanh.
|
||||
| Tình huống | Lệnh |
|
||||
|----------|---------|
|
||||
| Có API key, muốn cài nhanh nhất | `zeroclaw onboard --api-key sk-... --provider openrouter` |
|
||||
| Muốn được hướng dẫn từng bước | `zeroclaw onboard --interactive` |
|
||||
| Muốn được hướng dẫn từng bước | `zeroclaw onboard` |
|
||||
| Đã có config, chỉ cần sửa kênh | `zeroclaw onboard --channels-only` |
|
||||
| Dùng xác thực subscription | Xem [Subscription Auth](../../../README.md#subscription-auth-openai-codex--claude-code) |
|
||||
|
||||
## Thiết lập và kiểm tra
|
||||
|
||||
- Thiết lập nhanh: `zeroclaw onboard --api-key "sk-..." --provider openrouter`
|
||||
- Thiết lập tương tác: `zeroclaw onboard --interactive`
|
||||
- Thiết lập hướng dẫn: `zeroclaw onboard`
|
||||
- Kiểm tra môi trường: `zeroclaw status` + `zeroclaw doctor`
|
||||
|
||||
## Tiếp theo
|
||||
|
||||
@@ -89,19 +89,13 @@ Lệnh này build image ZeroClaw cục bộ và chạy thiết lập trong conta
|
||||
### Thiết lập nhanh (không tương tác)
|
||||
|
||||
```bash
|
||||
./install.sh --onboard --api-key "sk-..." --provider openrouter
|
||||
./install.sh --api-key "sk-..." --provider openrouter
|
||||
```
|
||||
|
||||
Hoặc dùng biến môi trường:
|
||||
|
||||
```bash
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./install.sh --onboard
|
||||
```
|
||||
|
||||
### Thiết lập tương tác
|
||||
|
||||
```bash
|
||||
./install.sh --interactive-onboard
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./install.sh
|
||||
```
|
||||
|
||||
## Các cờ hữu ích
|
||||
|
||||
+453
-212
@@ -47,61 +47,89 @@ fi
|
||||
# --- From here on, we are running under bash ---
|
||||
set -euo pipefail
|
||||
|
||||
# --- Color and styling ---
|
||||
if [[ -t 1 ]]; then
|
||||
BLUE='\033[0;34m'
|
||||
BOLD_BLUE='\033[1;34m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
RED='\033[0;31m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
RESET='\033[0m'
|
||||
else
|
||||
BLUE='' BOLD_BLUE='' GREEN='' YELLOW='' RED='' BOLD='' DIM='' RESET=''
|
||||
fi
|
||||
|
||||
CRAB="🦀"
|
||||
|
||||
info() {
|
||||
echo "==> $*"
|
||||
echo -e "${BLUE}${CRAB}${RESET} ${BOLD}$*${RESET}"
|
||||
}
|
||||
|
||||
step_ok() {
|
||||
echo -e " ${GREEN}✓${RESET} $*"
|
||||
}
|
||||
|
||||
step_dot() {
|
||||
echo -e " ${DIM}·${RESET} $*"
|
||||
}
|
||||
|
||||
step_fail() {
|
||||
echo -e " ${RED}✗${RESET} $*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo "warning: $*" >&2
|
||||
echo -e "${YELLOW}!${RESET} $*" >&2
|
||||
}
|
||||
|
||||
error() {
|
||||
echo "error: $*" >&2
|
||||
echo -e "${RED}✗${RESET} ${RED}$*${RESET}" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
ZeroClaw installer
|
||||
ZeroClaw installer — one-click bootstrap
|
||||
|
||||
Usage:
|
||||
./install.sh [options]
|
||||
|
||||
Modes:
|
||||
Default mode installs/builds ZeroClaw only (requires existing Rust toolchain).
|
||||
Guided mode asks setup questions and configures options interactively.
|
||||
Optional bootstrap mode can also install system dependencies and Rust.
|
||||
The installer builds ZeroClaw, configures your provider and API key,
|
||||
starts the gateway service, and opens the dashboard — all in one step.
|
||||
|
||||
Options:
|
||||
--guided Run interactive guided installer
|
||||
--guided Run interactive guided installer (default on Linux TTY)
|
||||
--no-guided Disable guided installer
|
||||
--docker Run install in Docker-compatible mode and launch onboarding inside the container
|
||||
--docker Run install in Docker-compatible mode
|
||||
--install-system-deps Install build dependencies (Linux/macOS)
|
||||
--install-rust Install Rust via rustup if missing
|
||||
--prefer-prebuilt Try latest release binary first; fallback to source build on miss
|
||||
--prebuilt-only Install only from latest release binary (no source build fallback)
|
||||
--force-source-build Disable prebuilt flow and always build from source
|
||||
--onboard Run onboarding after install
|
||||
--interactive-onboard Run interactive onboarding (implies --onboard)
|
||||
--api-key <key> API key for non-interactive onboarding
|
||||
--provider <id> Provider for non-interactive onboarding (default: openrouter)
|
||||
--model <id> Model for non-interactive onboarding (optional)
|
||||
--api-key <key> API key (skips interactive prompt)
|
||||
--provider <id> Provider (default: openrouter)
|
||||
--model <id> Model (optional)
|
||||
--skip-onboard Skip provider/API key configuration
|
||||
--skip-build Skip build step
|
||||
--skip-install Skip cargo install step
|
||||
--build-first Alias for explicitly enabling separate `cargo build --release --locked`
|
||||
--skip-build Skip build step (`cargo build --release --locked` or Docker image build)
|
||||
--skip-install Skip `cargo install --path . --force --locked`
|
||||
-h, --help Show help
|
||||
|
||||
Examples:
|
||||
./install.sh
|
||||
./install.sh --guided
|
||||
./install.sh --install-system-deps --install-rust
|
||||
./install.sh --prefer-prebuilt
|
||||
./install.sh --prebuilt-only
|
||||
./install.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"]
|
||||
./install.sh --interactive-onboard
|
||||
# One-click install (interactive)
|
||||
curl -fsSL https://zeroclawlabs.ai/install.sh | bash
|
||||
|
||||
# Non-interactive with API key
|
||||
./install.sh --api-key "sk-..." --provider openrouter
|
||||
|
||||
# Prebuilt binary (fastest)
|
||||
./install.sh --prefer-prebuilt --api-key "sk-..."
|
||||
|
||||
# Docker deploy
|
||||
./install.sh --docker
|
||||
|
||||
# Remote one-liner
|
||||
curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh | bash
|
||||
# Build only, configure later
|
||||
./install.sh --skip-onboard
|
||||
|
||||
Environment:
|
||||
ZEROCLAW_CONTAINER_CLI Container CLI command (default: docker; auto-fallback: podman)
|
||||
@@ -159,7 +187,12 @@ detect_release_target() {
|
||||
echo "x86_64-unknown-linux-gnu"
|
||||
;;
|
||||
Linux:aarch64|Linux:arm64)
|
||||
echo "aarch64-unknown-linux-gnu"
|
||||
# Termux on Android needs the android target, not linux-gnu
|
||||
if [[ -n "${TERMUX_VERSION:-}" || -d "/data/data/com.termux" ]]; then
|
||||
echo "aarch64-linux-android"
|
||||
else
|
||||
echo "aarch64-unknown-linux-gnu"
|
||||
fi
|
||||
;;
|
||||
Linux:armv7l|Linux:armv6l)
|
||||
echo "armv7-unknown-linux-gnueabihf"
|
||||
@@ -268,7 +301,7 @@ install_prebuilt_binary() {
|
||||
temp_dir="$(mktemp -d -t zeroclaw-prebuilt-XXXXXX)"
|
||||
archive_path="$temp_dir/${asset_name}"
|
||||
|
||||
info "Attempting pre-built binary install for target: $target"
|
||||
step_dot "Attempting pre-built binary install for target: $target"
|
||||
if ! curl -fsSL "$archive_url" -o "$archive_path"; then
|
||||
warn "Could not download release asset: $archive_url"
|
||||
rm -rf "$temp_dir"
|
||||
@@ -296,7 +329,7 @@ install_prebuilt_binary() {
|
||||
install -m 0755 "$extracted_bin" "$install_dir/zeroclaw"
|
||||
rm -rf "$temp_dir"
|
||||
|
||||
info "Installed pre-built binary to $install_dir/zeroclaw"
|
||||
step_ok "Installed pre-built binary to $install_dir/zeroclaw"
|
||||
if [[ ":$PATH:" != *":$install_dir:"* ]]; then
|
||||
warn "$install_dir is not in PATH for this shell."
|
||||
warn "Run: export PATH=\"$install_dir:\$PATH\""
|
||||
@@ -392,11 +425,18 @@ bool_to_word() {
|
||||
}
|
||||
|
||||
guided_input_stream() {
|
||||
if [[ -t 0 ]]; then
|
||||
# Some constrained containers report interactive stdin (-t 0) but deny
|
||||
# opening /dev/stdin directly. Probe readability before selecting it.
|
||||
if [[ -t 0 ]] && (: </dev/stdin) 2>/dev/null; then
|
||||
echo "/dev/stdin"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -t 0 ]] && (: </proc/self/fd/0) 2>/dev/null; then
|
||||
echo "/proc/self/fd/0"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (: </dev/tty) 2>/dev/null; then
|
||||
echo "/dev/tty"
|
||||
return 0
|
||||
@@ -463,16 +503,16 @@ prompt_yes_no() {
|
||||
}
|
||||
|
||||
install_system_deps() {
|
||||
info "Installing system dependencies"
|
||||
step_dot "Installing system dependencies"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
if have_cmd apk; then
|
||||
find_missing_alpine_prereqs
|
||||
if [[ ${#ALPINE_MISSING_PKGS[@]} -eq 0 ]]; then
|
||||
info "Alpine prerequisites already installed"
|
||||
step_ok "Alpine prerequisites already installed"
|
||||
else
|
||||
info "Installing Alpine prerequisites: ${ALPINE_MISSING_PKGS[*]}"
|
||||
step_dot "Installing Alpine prerequisites: ${ALPINE_MISSING_PKGS[*]}"
|
||||
run_privileged apk add --no-cache "${ALPINE_MISSING_PKGS[@]}"
|
||||
fi
|
||||
elif have_cmd apt-get; then
|
||||
@@ -499,13 +539,15 @@ install_system_deps() {
|
||||
openssl \
|
||||
perl \
|
||||
ca-certificates
|
||||
elif have_cmd pkg && [[ -n "${TERMUX_VERSION:-}" ]]; then
|
||||
pkg install -y build-essential pkg-config git curl openssl perl
|
||||
else
|
||||
warn "Unsupported Linux distribution. Install compiler toolchain + pkg-config + git + curl + OpenSSL headers + perl manually."
|
||||
fi
|
||||
;;
|
||||
Darwin)
|
||||
if ! xcode-select -p >/dev/null 2>&1; then
|
||||
info "Installing Xcode Command Line Tools"
|
||||
step_dot "Installing Xcode Command Line Tools"
|
||||
xcode-select --install || true
|
||||
cat <<'MSG'
|
||||
Please complete the Xcode Command Line Tools installation dialog,
|
||||
@@ -525,7 +567,7 @@ MSG
|
||||
|
||||
install_rust_toolchain() {
|
||||
if have_cmd cargo && have_cmd rustc; then
|
||||
info "Rust already installed: $(rustc --version)"
|
||||
step_ok "Rust already installed: $(rustc --version)"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -534,7 +576,7 @@ install_rust_toolchain() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Installing Rust via rustup"
|
||||
step_dot "Installing Rust via rustup"
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
|
||||
if [[ -f "$HOME/.cargo/env" ]]; then
|
||||
@@ -549,11 +591,98 @@ install_rust_toolchain() {
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_provider() {
|
||||
local provider_input=""
|
||||
echo
|
||||
echo -e " ${BOLD}Select your AI provider${RESET}"
|
||||
echo -e " ${DIM}(press Enter for default: ${PROVIDER})${RESET}"
|
||||
echo
|
||||
echo -e " ${BOLD_BLUE}1)${RESET} OpenRouter ${DIM}(recommended — multi-model gateway)${RESET}"
|
||||
echo -e " ${BOLD_BLUE}2)${RESET} Anthropic ${DIM}(Claude)${RESET}"
|
||||
echo -e " ${BOLD_BLUE}3)${RESET} OpenAI ${DIM}(GPT)${RESET}"
|
||||
echo -e " ${BOLD_BLUE}4)${RESET} Gemini ${DIM}(Google)${RESET}"
|
||||
echo -e " ${BOLD_BLUE}5)${RESET} Ollama ${DIM}(local, no API key needed)${RESET}"
|
||||
echo -e " ${BOLD_BLUE}6)${RESET} Groq ${DIM}(fast inference)${RESET}"
|
||||
echo -e " ${BOLD_BLUE}7)${RESET} Venice ${DIM}(privacy-focused)${RESET}"
|
||||
echo -e " ${BOLD_BLUE}8)${RESET} Other ${DIM}(enter provider ID manually)${RESET}"
|
||||
echo
|
||||
|
||||
if ! guided_read provider_input " Provider [1]: "; then
|
||||
error "input was interrupted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${provider_input:-1}" in
|
||||
1|"") PROVIDER="openrouter" ;;
|
||||
2) PROVIDER="anthropic" ;;
|
||||
3) PROVIDER="openai" ;;
|
||||
4) PROVIDER="gemini" ;;
|
||||
5) PROVIDER="ollama" ;;
|
||||
6) PROVIDER="groq" ;;
|
||||
7) PROVIDER="venice" ;;
|
||||
8)
|
||||
if ! guided_read provider_input " Provider ID: "; then
|
||||
error "input was interrupted."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$provider_input" ]]; then
|
||||
PROVIDER="$provider_input"
|
||||
fi
|
||||
;;
|
||||
*) PROVIDER="openrouter" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
prompt_api_key() {
|
||||
local api_key_input=""
|
||||
|
||||
if [[ "$PROVIDER" == "ollama" ]]; then
|
||||
step_ok "Ollama selected — no API key required"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo
|
||||
if [[ -n "$API_KEY" ]]; then
|
||||
step_ok "API key provided via environment/flag"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e " ${BOLD}Enter your ${PROVIDER} API key${RESET}"
|
||||
echo -e " ${DIM}(input is hidden; leave empty to configure later)${RESET}"
|
||||
echo
|
||||
|
||||
if ! guided_read api_key_input " API key: " true; then
|
||||
echo
|
||||
error "input was interrupted."
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
|
||||
if [[ -n "$api_key_input" ]]; then
|
||||
API_KEY="$api_key_input"
|
||||
step_ok "API key set"
|
||||
else
|
||||
warn "No API key entered — you can configure it later with zeroclaw onboard"
|
||||
SKIP_ONBOARD=true
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_model() {
|
||||
local model_input=""
|
||||
|
||||
echo -e " ${DIM}Model (press Enter for provider default):${RESET}"
|
||||
if ! guided_read model_input " Model [default]: "; then
|
||||
error "input was interrupted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$model_input" ]]; then
|
||||
MODEL="$model_input"
|
||||
fi
|
||||
}
|
||||
|
||||
run_guided_installer() {
|
||||
local os_name="$1"
|
||||
local provider_input=""
|
||||
local model_input=""
|
||||
local api_key_input=""
|
||||
|
||||
if ! guided_input_stream >/dev/null; then
|
||||
error "guided installer requires an interactive terminal."
|
||||
@@ -562,10 +691,11 @@ run_guided_installer() {
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "ZeroClaw guided installer"
|
||||
echo "Answer a few questions, then the installer will run automatically."
|
||||
echo -e " ${BOLD_BLUE}${CRAB} ZeroClaw Guided Installer${RESET}"
|
||||
echo -e " ${DIM}Answer a few questions, then the installer will handle everything.${RESET}"
|
||||
echo
|
||||
|
||||
# --- System dependencies ---
|
||||
if [[ "$os_name" == "Linux" ]]; then
|
||||
if prompt_yes_no "Install Linux build dependencies (toolchain/pkg-config/git/curl)?" "yes"; then
|
||||
INSTALL_SYSTEM_DEPS=true
|
||||
@@ -576,89 +706,34 @@ run_guided_installer() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Rust toolchain ---
|
||||
if have_cmd cargo && have_cmd rustc; then
|
||||
info "Detected Rust toolchain: $(rustc --version)"
|
||||
step_ok "Detected Rust toolchain: $(rustc --version)"
|
||||
else
|
||||
if prompt_yes_no "Rust toolchain not found. Install Rust via rustup now?" "yes"; then
|
||||
INSTALL_RUST=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if prompt_yes_no "Run a separate prebuild before install?" "yes"; then
|
||||
SKIP_BUILD=false
|
||||
else
|
||||
SKIP_BUILD=true
|
||||
fi
|
||||
|
||||
if prompt_yes_no "Install zeroclaw into cargo bin now?" "yes"; then
|
||||
SKIP_INSTALL=false
|
||||
else
|
||||
SKIP_INSTALL=true
|
||||
fi
|
||||
|
||||
if prompt_yes_no "Run onboarding after install?" "no"; then
|
||||
RUN_ONBOARD=true
|
||||
if prompt_yes_no "Use interactive onboarding?" "yes"; then
|
||||
INTERACTIVE_ONBOARD=true
|
||||
else
|
||||
INTERACTIVE_ONBOARD=false
|
||||
if ! guided_read provider_input "Provider [$PROVIDER]: "; then
|
||||
error "guided installer input was interrupted."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$provider_input" ]]; then
|
||||
PROVIDER="$provider_input"
|
||||
fi
|
||||
|
||||
if ! guided_read model_input "Model [${MODEL:-leave empty}]: "; then
|
||||
error "guided installer input was interrupted."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$model_input" ]]; then
|
||||
MODEL="$model_input"
|
||||
fi
|
||||
|
||||
if [[ -z "$API_KEY" ]]; then
|
||||
if ! guided_read api_key_input "API key (hidden, leave empty to switch to interactive onboarding): " true; then
|
||||
echo
|
||||
error "guided installer input was interrupted."
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
if [[ -n "$api_key_input" ]]; then
|
||||
API_KEY="$api_key_input"
|
||||
else
|
||||
warn "No API key entered. Using interactive onboarding instead."
|
||||
INTERACTIVE_ONBOARD=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# --- Provider + API key (inline onboarding) ---
|
||||
prompt_provider
|
||||
prompt_api_key
|
||||
prompt_model
|
||||
|
||||
# --- Install plan summary ---
|
||||
echo
|
||||
info "Installer plan"
|
||||
local install_binary=true
|
||||
local build_first=false
|
||||
if [[ "$SKIP_INSTALL" == true ]]; then
|
||||
install_binary=false
|
||||
echo -e "${BOLD}Install plan${RESET}"
|
||||
step_dot "OS: $(echo "$os_name" | tr '[:upper:]' '[:lower:]')"
|
||||
step_dot "Install system deps: $(bool_to_word "$INSTALL_SYSTEM_DEPS")"
|
||||
step_dot "Install Rust: $(bool_to_word "$INSTALL_RUST")"
|
||||
step_dot "Provider: ${PROVIDER}"
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
step_dot "Model: ${MODEL}"
|
||||
fi
|
||||
if [[ "$SKIP_BUILD" == false ]]; then
|
||||
build_first=true
|
||||
fi
|
||||
echo " docker-mode: $(bool_to_word "$DOCKER_MODE")"
|
||||
echo " install-system-deps: $(bool_to_word "$INSTALL_SYSTEM_DEPS")"
|
||||
echo " install-rust: $(bool_to_word "$INSTALL_RUST")"
|
||||
echo " build-first: $(bool_to_word "$build_first")"
|
||||
echo " install-binary: $(bool_to_word "$install_binary")"
|
||||
echo " onboard: $(bool_to_word "$RUN_ONBOARD")"
|
||||
if [[ "$RUN_ONBOARD" == true ]]; then
|
||||
echo " interactive-onboard: $(bool_to_word "$INTERACTIVE_ONBOARD")"
|
||||
if [[ "$INTERACTIVE_ONBOARD" == false ]]; then
|
||||
echo " provider: $PROVIDER"
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
echo " model: $MODEL"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$API_KEY" ]]; then
|
||||
step_ok "API key: configured"
|
||||
else
|
||||
step_dot "API key: not set (configure later)"
|
||||
fi
|
||||
|
||||
echo
|
||||
@@ -758,42 +833,37 @@ run_docker_bootstrap() {
|
||||
info "Container CLI: $CONTAINER_CLI"
|
||||
|
||||
local onboard_cmd=()
|
||||
if [[ "$INTERACTIVE_ONBOARD" == true ]]; then
|
||||
info "Launching interactive onboarding in container"
|
||||
onboard_cmd=(onboard --interactive)
|
||||
else
|
||||
if [[ -z "$API_KEY" ]]; then
|
||||
cat <<'MSG'
|
||||
==> Onboarding requested, but API key not provided.
|
||||
Use either:
|
||||
--api-key "sk-..."
|
||||
or:
|
||||
ZEROCLAW_API_KEY="sk-..." ./install.sh --docker
|
||||
or run interactive:
|
||||
./install.sh --docker --interactive-onboard
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$SKIP_ONBOARD" == true ]]; then
|
||||
info "Skipping onboarding in container"
|
||||
onboard_cmd=()
|
||||
elif [[ -n "$API_KEY" ]]; then
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
info "Launching quick onboarding in container (provider: $PROVIDER, model: $MODEL)"
|
||||
info "Configuring provider in container (provider: $PROVIDER, model: $MODEL)"
|
||||
else
|
||||
info "Launching quick onboarding in container (provider: $PROVIDER)"
|
||||
info "Configuring provider in container (provider: $PROVIDER)"
|
||||
fi
|
||||
onboard_cmd=(onboard --api-key "$API_KEY" --provider "$PROVIDER")
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
onboard_cmd+=(--model "$MODEL")
|
||||
fi
|
||||
else
|
||||
info "Launching setup in container"
|
||||
onboard_cmd=(onboard --provider "$PROVIDER")
|
||||
fi
|
||||
|
||||
"$CONTAINER_CLI" run --rm -it \
|
||||
"${container_run_namespace_args[@]+"${container_run_namespace_args[@]}"}" \
|
||||
"${container_run_user_args[@]}" \
|
||||
-e HOME=/zeroclaw-data \
|
||||
-e ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace \
|
||||
-v "$config_mount" \
|
||||
-v "$workspace_mount" \
|
||||
"$docker_image" \
|
||||
"${onboard_cmd[@]}"
|
||||
if [[ ${#onboard_cmd[@]} -gt 0 ]]; then
|
||||
"$CONTAINER_CLI" run --rm -it \
|
||||
"${container_run_namespace_args[@]+"${container_run_namespace_args[@]}"}" \
|
||||
"${container_run_user_args[@]}" \
|
||||
-e HOME=/zeroclaw-data \
|
||||
-e ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace \
|
||||
-v "$config_mount" \
|
||||
-v "$workspace_mount" \
|
||||
"$docker_image" \
|
||||
"${onboard_cmd[@]}"
|
||||
else
|
||||
info "Docker image ready. Run zeroclaw onboard inside the container to configure."
|
||||
fi
|
||||
}
|
||||
|
||||
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
|
||||
@@ -809,8 +879,7 @@ INSTALL_RUST=false
|
||||
PREFER_PREBUILT=false
|
||||
PREBUILT_ONLY=false
|
||||
FORCE_SOURCE_BUILD=false
|
||||
RUN_ONBOARD=false
|
||||
INTERACTIVE_ONBOARD=false
|
||||
SKIP_ONBOARD=false
|
||||
SKIP_BUILD=false
|
||||
SKIP_INSTALL=false
|
||||
PREBUILT_INSTALLED=false
|
||||
@@ -853,13 +922,8 @@ while [[ $# -gt 0 ]]; do
|
||||
FORCE_SOURCE_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--onboard)
|
||||
RUN_ONBOARD=true
|
||||
shift
|
||||
;;
|
||||
--interactive-onboard)
|
||||
RUN_ONBOARD=true
|
||||
INTERACTIVE_ONBOARD=true
|
||||
--skip-onboard)
|
||||
SKIP_ONBOARD=true
|
||||
shift
|
||||
;;
|
||||
--api-key)
|
||||
@@ -990,8 +1054,51 @@ if [[ ! -f "$WORK_DIR/Cargo.toml" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
info "ZeroClaw installer"
|
||||
echo " workspace: $WORK_DIR"
|
||||
echo
|
||||
echo -e " ${BOLD_BLUE}${CRAB} ZeroClaw Installer${RESET}"
|
||||
echo -e " ${DIM}Build it, run it, trust it.${RESET}"
|
||||
echo
|
||||
step_ok "Detected: ${BOLD}$(echo "$OS_NAME" | tr '[:upper:]' '[:lower:]')${RESET}"
|
||||
|
||||
# --- Detect existing installation and version ---
|
||||
EXISTING_VERSION=""
|
||||
INSTALL_MODE="fresh"
|
||||
if have_cmd zeroclaw; then
|
||||
EXISTING_VERSION="$(zeroclaw --version 2>/dev/null | awk '{print $NF}' || true)"
|
||||
INSTALL_MODE="upgrade"
|
||||
elif [[ -x "$HOME/.cargo/bin/zeroclaw" ]]; then
|
||||
EXISTING_VERSION="$("$HOME/.cargo/bin/zeroclaw" --version 2>/dev/null | awk '{print $NF}' || true)"
|
||||
INSTALL_MODE="upgrade"
|
||||
fi
|
||||
|
||||
# Determine install method
|
||||
if [[ "$DOCKER_MODE" == true ]]; then
|
||||
INSTALL_METHOD="docker"
|
||||
elif [[ "$PREBUILT_ONLY" == true || "$PREFER_PREBUILT" == true ]]; then
|
||||
INSTALL_METHOD="prebuilt binary"
|
||||
else
|
||||
INSTALL_METHOD="source (cargo)"
|
||||
fi
|
||||
|
||||
# Determine target version from Cargo.toml
|
||||
TARGET_VERSION=""
|
||||
if [[ -f "$WORK_DIR/Cargo.toml" ]]; then
|
||||
TARGET_VERSION="$(grep -m1 '^version' "$WORK_DIR/Cargo.toml" | sed 's/.*"\(.*\)".*/\1/' || true)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${BOLD}Install plan${RESET}"
|
||||
step_dot "OS: $(echo "$OS_NAME" | tr '[:upper:]' '[:lower:]')"
|
||||
step_dot "Install method: ${INSTALL_METHOD}"
|
||||
if [[ -n "$TARGET_VERSION" ]]; then
|
||||
step_dot "Requested version: v${TARGET_VERSION}"
|
||||
fi
|
||||
step_dot "Workspace: $WORK_DIR"
|
||||
if [[ "$INSTALL_MODE" == "upgrade" && -n "$EXISTING_VERSION" ]]; then
|
||||
step_dot "Existing ZeroClaw installation detected, upgrading from v${EXISTING_VERSION}"
|
||||
elif [[ "$INSTALL_MODE" == "upgrade" ]]; then
|
||||
step_dot "Existing ZeroClaw installation detected, upgrading"
|
||||
fi
|
||||
|
||||
cd "$WORK_DIR"
|
||||
|
||||
@@ -1006,26 +1113,21 @@ fi
|
||||
|
||||
if [[ "$DOCKER_MODE" == true ]]; then
|
||||
ensure_docker_ready
|
||||
if [[ "$RUN_ONBOARD" == false ]]; then
|
||||
RUN_ONBOARD=true
|
||||
if [[ -z "$API_KEY" ]]; then
|
||||
INTERACTIVE_ONBOARD=true
|
||||
fi
|
||||
fi
|
||||
run_docker_bootstrap
|
||||
cat <<'DONE'
|
||||
|
||||
✅ Docker bootstrap complete.
|
||||
|
||||
Your containerized ZeroClaw data is persisted under:
|
||||
DONE
|
||||
echo " $DOCKER_DATA_DIR"
|
||||
cat <<'DONE'
|
||||
|
||||
Next steps:
|
||||
./install.sh --docker --interactive-onboard
|
||||
./install.sh --docker --api-key "sk-..." --provider openrouter
|
||||
DONE
|
||||
echo
|
||||
echo -e "${BOLD_BLUE}${CRAB} Docker bootstrap complete!${RESET}"
|
||||
echo
|
||||
echo -e "${BOLD}Your containerized ZeroClaw data is persisted under:${RESET}"
|
||||
echo -e " ${DIM}$DOCKER_DATA_DIR${RESET}"
|
||||
echo
|
||||
echo -e "${BOLD}Dashboard URL:${RESET} ${BLUE}http://127.0.0.1:42617${RESET}"
|
||||
echo
|
||||
echo -e "${BOLD}Next steps:${RESET}"
|
||||
echo -e " ${DIM}zeroclaw status${RESET}"
|
||||
echo -e " ${DIM}zeroclaw agent -m \"Hello, ZeroClaw!\"${RESET}"
|
||||
echo -e " ${DIM}zeroclaw gateway${RESET}"
|
||||
echo
|
||||
echo -e "${BOLD}Docs:${RESET} ${BLUE}https://www.zeroclawlabs.ai/docs${RESET}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -1062,18 +1164,56 @@ MSG
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_BUILD" == false ]]; then
|
||||
info "Building release binary"
|
||||
cargo build --release --locked
|
||||
echo
|
||||
echo -e "${BOLD_BLUE}[1/3]${RESET} ${BOLD}Preparing environment${RESET}"
|
||||
if [[ "$INSTALL_SYSTEM_DEPS" == true ]]; then
|
||||
step_ok "System dependencies installed"
|
||||
else
|
||||
info "Skipping build"
|
||||
step_ok "System dependencies satisfied"
|
||||
fi
|
||||
if have_cmd cargo && have_cmd rustc; then
|
||||
step_ok "Rust $(rustc --version | awk '{print $2}') found"
|
||||
step_dot "Active Rust: $(rustc --version) ($(command -v rustc))"
|
||||
step_dot "Active cargo: $(cargo --version | awk '{print $2}') ($(command -v cargo))"
|
||||
else
|
||||
step_dot "Rust not detected"
|
||||
fi
|
||||
if have_cmd git; then
|
||||
step_ok "Git already installed"
|
||||
else
|
||||
step_dot "Git not found"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${BOLD_BLUE}[2/3]${RESET} ${BOLD}Installing ZeroClaw${RESET}"
|
||||
if [[ -n "$TARGET_VERSION" ]]; then
|
||||
step_dot "Installing ZeroClaw v${TARGET_VERSION}"
|
||||
fi
|
||||
if [[ "$SKIP_BUILD" == false ]]; then
|
||||
step_dot "Building release binary"
|
||||
cargo build --release --locked
|
||||
step_ok "Release binary built"
|
||||
else
|
||||
step_dot "Skipping build"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_INSTALL" == false ]]; then
|
||||
info "Installing zeroclaw to cargo bin"
|
||||
step_dot "Installing zeroclaw to cargo bin"
|
||||
|
||||
# Clean up stale cargo install tracking from the old "zeroclaw" package name
|
||||
# (renamed to "zeroclawlabs"). Without this, `cargo install zeroclawlabs` from
|
||||
# crates.io fails with "binary already exists as part of `zeroclaw`".
|
||||
if have_cmd cargo; then
|
||||
if [[ -f "$HOME/.cargo/.crates.toml" ]] && grep -q '^"zeroclaw ' "$HOME/.cargo/.crates.toml" 2>/dev/null; then
|
||||
step_dot "Removing stale cargo tracking for old 'zeroclaw' package name"
|
||||
cargo uninstall zeroclaw 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
cargo install --path "$WORK_DIR" --force --locked
|
||||
step_ok "ZeroClaw installed"
|
||||
else
|
||||
info "Skipping install"
|
||||
step_dot "Skipping install"
|
||||
fi
|
||||
|
||||
ZEROCLAW_BIN=""
|
||||
@@ -1085,48 +1225,149 @@ elif [[ -x "$WORK_DIR/target/release/zeroclaw" ]]; then
|
||||
ZEROCLAW_BIN="$WORK_DIR/target/release/zeroclaw"
|
||||
fi
|
||||
|
||||
if [[ "$RUN_ONBOARD" == true ]]; then
|
||||
if [[ -z "$ZEROCLAW_BIN" ]]; then
|
||||
error "onboarding requested but zeroclaw binary is not available."
|
||||
error "Run without --skip-install, or ensure zeroclaw is in PATH."
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
echo -e "${BOLD_BLUE}[3/3]${RESET} ${BOLD}Finalizing setup${RESET}"
|
||||
|
||||
if [[ "$INTERACTIVE_ONBOARD" == true ]]; then
|
||||
info "Running interactive onboarding"
|
||||
"$ZEROCLAW_BIN" onboard --interactive
|
||||
else
|
||||
if [[ -z "$API_KEY" ]]; then
|
||||
cat <<'MSG'
|
||||
==> Onboarding requested, but API key not provided.
|
||||
Use either:
|
||||
--api-key "sk-..."
|
||||
or:
|
||||
ZEROCLAW_API_KEY="sk-..." ./install.sh --onboard
|
||||
or run interactive:
|
||||
./install.sh --interactive-onboard
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
info "Running quick onboarding (provider: $PROVIDER, model: $MODEL)"
|
||||
else
|
||||
info "Running quick onboarding (provider: $PROVIDER)"
|
||||
fi
|
||||
# --- Inline onboarding (provider + API key configuration) ---
|
||||
if [[ "$SKIP_ONBOARD" == false && -n "$ZEROCLAW_BIN" ]]; then
|
||||
if [[ -n "$API_KEY" ]]; then
|
||||
step_dot "Configuring provider: ${PROVIDER}"
|
||||
ONBOARD_CMD=("$ZEROCLAW_BIN" onboard --api-key "$API_KEY" --provider "$PROVIDER")
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
ONBOARD_CMD+=(--model "$MODEL")
|
||||
fi
|
||||
"${ONBOARD_CMD[@]}"
|
||||
if "${ONBOARD_CMD[@]}" 2>/dev/null; then
|
||||
step_ok "Provider configured"
|
||||
else
|
||||
step_fail "Provider configuration failed — run zeroclaw onboard to retry"
|
||||
fi
|
||||
elif [[ "$PROVIDER" == "ollama" ]]; then
|
||||
step_dot "Configuring Ollama (no API key needed)"
|
||||
if "$ZEROCLAW_BIN" onboard --provider ollama 2>/dev/null; then
|
||||
step_ok "Ollama configured"
|
||||
else
|
||||
step_fail "Ollama configuration failed — run zeroclaw onboard to retry"
|
||||
fi
|
||||
else
|
||||
# No API key and not ollama — prompt inline if interactive, skip otherwise
|
||||
if [[ -t 0 && -t 1 ]]; then
|
||||
prompt_provider
|
||||
prompt_api_key
|
||||
if [[ -n "$API_KEY" ]]; then
|
||||
ONBOARD_CMD=("$ZEROCLAW_BIN" onboard --api-key "$API_KEY" --provider "$PROVIDER")
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
ONBOARD_CMD+=(--model "$MODEL")
|
||||
fi
|
||||
if "${ONBOARD_CMD[@]}" 2>/dev/null; then
|
||||
step_ok "Provider configured"
|
||||
else
|
||||
step_fail "Provider configuration failed — run zeroclaw onboard to retry"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
step_dot "No API key provided — run zeroclaw onboard to configure"
|
||||
fi
|
||||
fi
|
||||
elif [[ "$SKIP_ONBOARD" == true ]]; then
|
||||
step_dot "Skipping configuration (run zeroclaw onboard later)"
|
||||
elif [[ -z "$ZEROCLAW_BIN" ]]; then
|
||||
warn "ZeroClaw binary not found — cannot configure provider"
|
||||
fi
|
||||
|
||||
# --- Gateway service management ---
|
||||
if [[ -n "$ZEROCLAW_BIN" ]]; then
|
||||
# Try to install and start the gateway service
|
||||
step_dot "Checking gateway service"
|
||||
if "$ZEROCLAW_BIN" service install 2>/dev/null; then
|
||||
step_ok "Gateway service installed"
|
||||
if "$ZEROCLAW_BIN" service restart 2>/dev/null; then
|
||||
step_ok "Gateway service restarted"
|
||||
else
|
||||
step_fail "Gateway service restart failed — re-run with zeroclaw service start"
|
||||
fi
|
||||
else
|
||||
step_dot "Gateway service not installed (run zeroclaw service install later)"
|
||||
fi
|
||||
|
||||
# --- Post-install doctor check ---
|
||||
step_dot "Running doctor to validate installation"
|
||||
if "$ZEROCLAW_BIN" doctor 2>/dev/null; then
|
||||
step_ok "Doctor complete"
|
||||
else
|
||||
warn "Doctor reported issues — run zeroclaw doctor --fix to resolve"
|
||||
fi
|
||||
fi
|
||||
|
||||
cat <<'DONE'
|
||||
# --- Determine installed version ---
|
||||
INSTALLED_VERSION=""
|
||||
if [[ -n "$ZEROCLAW_BIN" ]]; then
|
||||
INSTALLED_VERSION="$("$ZEROCLAW_BIN" --version 2>/dev/null | awk '{print $NF}' || true)"
|
||||
fi
|
||||
|
||||
✅ Bootstrap complete.
|
||||
# --- Success banner ---
|
||||
echo
|
||||
if [[ -n "$INSTALLED_VERSION" ]]; then
|
||||
echo -e "${BOLD_BLUE}${CRAB} ZeroClaw installed successfully (ZeroClaw ${INSTALLED_VERSION})!${RESET}"
|
||||
else
|
||||
echo -e "${BOLD_BLUE}${CRAB} ZeroClaw installed successfully!${RESET}"
|
||||
fi
|
||||
|
||||
Next steps:
|
||||
zeroclaw status
|
||||
zeroclaw agent -m "Hello, ZeroClaw!"
|
||||
zeroclaw gateway
|
||||
DONE
|
||||
if [[ "$INSTALL_MODE" == "upgrade" ]]; then
|
||||
step_dot "Upgrade complete"
|
||||
fi
|
||||
|
||||
# --- Dashboard URL ---
|
||||
GATEWAY_PORT=42617
|
||||
DASHBOARD_URL="http://127.0.0.1:${GATEWAY_PORT}"
|
||||
echo
|
||||
echo -e "${BOLD}Dashboard URL:${RESET} ${BLUE}${DASHBOARD_URL}${RESET}"
|
||||
echo -e "${DIM} Enter the pairing code shown above to connect.${RESET}"
|
||||
|
||||
# --- Copy to clipboard ---
|
||||
COPIED_TO_CLIPBOARD=false
|
||||
if [[ -t 1 ]]; then
|
||||
case "$OS_NAME" in
|
||||
Darwin)
|
||||
if have_cmd pbcopy; then
|
||||
printf '%s' "$DASHBOARD_URL" | pbcopy 2>/dev/null && COPIED_TO_CLIPBOARD=true
|
||||
fi
|
||||
;;
|
||||
Linux)
|
||||
if have_cmd xclip; then
|
||||
printf '%s' "$DASHBOARD_URL" | xclip -selection clipboard 2>/dev/null && COPIED_TO_CLIPBOARD=true
|
||||
elif have_cmd xsel; then
|
||||
printf '%s' "$DASHBOARD_URL" | xsel --clipboard 2>/dev/null && COPIED_TO_CLIPBOARD=true
|
||||
elif have_cmd wl-copy; then
|
||||
printf '%s' "$DASHBOARD_URL" | wl-copy 2>/dev/null && COPIED_TO_CLIPBOARD=true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [[ "$COPIED_TO_CLIPBOARD" == true ]]; then
|
||||
step_ok "Copied to clipboard"
|
||||
fi
|
||||
|
||||
# --- Open in browser ---
|
||||
if [[ -t 1 ]]; then
|
||||
case "$OS_NAME" in
|
||||
Darwin)
|
||||
if have_cmd open; then
|
||||
open "$DASHBOARD_URL" 2>/dev/null && step_ok "Opened in your browser"
|
||||
fi
|
||||
;;
|
||||
Linux)
|
||||
if have_cmd xdg-open; then
|
||||
xdg-open "$DASHBOARD_URL" 2>/dev/null && step_ok "Opened in your browser"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${BOLD}Next steps:${RESET}"
|
||||
echo -e " ${DIM}zeroclaw status${RESET}"
|
||||
echo -e " ${DIM}zeroclaw agent -m \"Hello, ZeroClaw!\"${RESET}"
|
||||
echo -e " ${DIM}zeroclaw gateway${RESET}"
|
||||
echo
|
||||
echo -e "${BOLD}Docs:${RESET} ${BLUE}https://www.zeroclawlabs.ai/docs${RESET}"
|
||||
echo
|
||||
|
||||
+227
-20
@@ -195,6 +195,18 @@ const COMPACTION_MAX_SOURCE_CHARS: usize = 12_000;
|
||||
/// Max characters retained in stored compaction summary.
|
||||
const COMPACTION_MAX_SUMMARY_CHARS: usize = 2_000;
|
||||
|
||||
/// Estimate token count for a message history using ~4 chars/token heuristic.
|
||||
/// Includes a small overhead per message for role/framing tokens.
|
||||
fn estimate_history_tokens(history: &[ChatMessage]) -> usize {
|
||||
history
|
||||
.iter()
|
||||
.map(|m| {
|
||||
// ~4 chars per token + ~4 framing tokens per message (role, delimiters)
|
||||
m.content.len().div_ceil(4) + 4
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Minimum interval between progress sends to avoid flooding the draft channel.
|
||||
pub(crate) const PROGRESS_MIN_INTERVAL_MS: u64 = 500;
|
||||
|
||||
@@ -288,6 +300,7 @@ async fn auto_compact_history(
|
||||
provider: &dyn Provider,
|
||||
model: &str,
|
||||
max_history: usize,
|
||||
max_context_tokens: usize,
|
||||
) -> Result<bool> {
|
||||
let has_system = history.first().map_or(false, |m| m.role == "system");
|
||||
let non_system_count = if has_system {
|
||||
@@ -296,7 +309,10 @@ async fn auto_compact_history(
|
||||
history.len()
|
||||
};
|
||||
|
||||
if non_system_count <= max_history {
|
||||
let estimated_tokens = estimate_history_tokens(history);
|
||||
|
||||
// Trigger compaction when either token budget OR message count is exceeded.
|
||||
if estimated_tokens <= max_context_tokens && non_system_count <= max_history {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -307,7 +323,16 @@ async fn auto_compact_history(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let compact_end = start + compact_count;
|
||||
let mut compact_end = start + compact_count;
|
||||
|
||||
// Snap compact_end to a user-turn boundary so we don't split mid-conversation.
|
||||
while compact_end > start && history.get(compact_end).map_or(false, |m| m.role != "user") {
|
||||
compact_end -= 1;
|
||||
}
|
||||
if compact_end <= start {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let to_compact: Vec<ChatMessage> = history[start..compact_end].to_vec();
|
||||
let transcript = build_compaction_transcript(&to_compact);
|
||||
|
||||
@@ -2112,14 +2137,7 @@ async fn execute_one_tool(
|
||||
observer: &dyn Observer,
|
||||
cancellation_token: Option<&CancellationToken>,
|
||||
) -> Result<ToolExecutionOutcome> {
|
||||
let args_summary = {
|
||||
let raw = call_arguments.to_string();
|
||||
if raw.len() > 300 {
|
||||
format!("{}…", &raw[..300])
|
||||
} else {
|
||||
raw
|
||||
}
|
||||
};
|
||||
let args_summary = truncate_with_ellipsis(&call_arguments.to_string(), 300);
|
||||
observer.record_event(&ObserverEvent::ToolCallStart {
|
||||
tool: call_name.to_string(),
|
||||
arguments: Some(args_summary),
|
||||
@@ -2642,6 +2660,15 @@ pub(crate) async fn run_tool_call_loop(
|
||||
"arguments": scrub_credentials(&tool_args.to_string()),
|
||||
}),
|
||||
);
|
||||
if let Some(ref tx) = on_delta {
|
||||
let _ = tx
|
||||
.send(format!(
|
||||
"\u{274c} {}: {}\n",
|
||||
call.name,
|
||||
truncate_with_ellipsis(&scrub_credentials(&cancelled), 200)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
ordered_results[idx] = Some((
|
||||
call.name.clone(),
|
||||
call.tool_call_id.clone(),
|
||||
@@ -2669,11 +2696,13 @@ pub(crate) async fn run_tool_call_loop(
|
||||
arguments: tool_args.clone(),
|
||||
};
|
||||
|
||||
// Only prompt interactively on CLI; auto-approve on other channels.
|
||||
let decision = if channel_name == "cli" {
|
||||
mgr.prompt_cli(&request)
|
||||
// Interactive CLI: prompt the operator.
|
||||
// Non-interactive (channels): auto-deny since no operator
|
||||
// is present to approve.
|
||||
let decision = if mgr.is_non_interactive() {
|
||||
ApprovalResponse::No
|
||||
} else {
|
||||
ApprovalResponse::Yes
|
||||
mgr.prompt_cli(&request)
|
||||
};
|
||||
|
||||
mgr.record_decision(&tool_name, &tool_args, decision, channel_name);
|
||||
@@ -2694,6 +2723,11 @@ pub(crate) async fn run_tool_call_loop(
|
||||
"arguments": scrub_credentials(&tool_args.to_string()),
|
||||
}),
|
||||
);
|
||||
if let Some(ref tx) = on_delta {
|
||||
let _ = tx
|
||||
.send(format!("\u{274c} {}: {}\n", tool_name, denied))
|
||||
.await;
|
||||
}
|
||||
ordered_results[idx] = Some((
|
||||
tool_name.clone(),
|
||||
call.tool_call_id.clone(),
|
||||
@@ -2730,6 +2764,11 @@ pub(crate) async fn run_tool_call_loop(
|
||||
"deduplicated": true,
|
||||
}),
|
||||
);
|
||||
if let Some(ref tx) = on_delta {
|
||||
let _ = tx
|
||||
.send(format!("\u{274c} {}: {}\n", tool_name, duplicate))
|
||||
.await;
|
||||
}
|
||||
ordered_results[idx] = Some((
|
||||
tool_name.clone(),
|
||||
call.tool_call_id.clone(),
|
||||
@@ -2832,13 +2871,19 @@ pub(crate) async fn run_tool_call_loop(
|
||||
// ── Progress: tool completion ───────────────────────
|
||||
if let Some(ref tx) = on_delta {
|
||||
let secs = outcome.duration.as_secs();
|
||||
let icon = if outcome.success {
|
||||
"\u{2705}"
|
||||
let progress_msg = if outcome.success {
|
||||
format!("\u{2705} {} ({secs}s)\n", call.name)
|
||||
} else if let Some(ref reason) = outcome.error_reason {
|
||||
format!(
|
||||
"\u{274c} {} ({secs}s): {}\n",
|
||||
call.name,
|
||||
truncate_with_ellipsis(reason, 200)
|
||||
)
|
||||
} else {
|
||||
"\u{274c}"
|
||||
format!("\u{274c} {} ({secs}s)\n", call.name)
|
||||
};
|
||||
tracing::debug!(tool = %call.name, secs, "Sending progress complete to draft");
|
||||
let _ = tx.send(format!("{icon} {} ({secs}s)\n", call.name)).await;
|
||||
let _ = tx.send(progress_msg).await;
|
||||
}
|
||||
|
||||
ordered_results[*idx] = Some((call.name.clone(), call.tool_call_id.clone(), outcome));
|
||||
@@ -2961,8 +3006,9 @@ pub async fn run(
|
||||
));
|
||||
|
||||
// ── Memory (the brain) ────────────────────────────────────────
|
||||
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory_with_storage(
|
||||
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory_with_storage_and_routes(
|
||||
&config.memory,
|
||||
&config.embedding_routes,
|
||||
Some(&config.storage.provider.config),
|
||||
&config.workspace_dir,
|
||||
config.api_key.as_deref(),
|
||||
@@ -3514,6 +3560,7 @@ pub async fn run(
|
||||
provider.as_ref(),
|
||||
model_name,
|
||||
config.agent.max_history_messages,
|
||||
config.agent.max_context_tokens,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -3554,8 +3601,9 @@ pub async fn process_message(config: Config, message: &str) -> Result<String> {
|
||||
&config.autonomy,
|
||||
&config.workspace_dir,
|
||||
));
|
||||
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory_with_storage(
|
||||
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory_with_storage_and_routes(
|
||||
&config.memory,
|
||||
&config.embedding_routes,
|
||||
Some(&config.storage.provider.config),
|
||||
&config.workspace_dir,
|
||||
config.api_key.as_deref(),
|
||||
@@ -3843,6 +3891,26 @@ mod tests {
|
||||
assert!(scrubbed.contains("\"api_key\": \"sk-1*[REDACTED]\""));
|
||||
assert!(scrubbed.contains("public"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_one_tool_does_not_panic_on_utf8_boundary() {
|
||||
let call_arguments = (0..600)
|
||||
.map(|n| serde_json::json!({ "content": format!("{}:tail", "a".repeat(n)) }))
|
||||
.find(|args| {
|
||||
let raw = args.to_string();
|
||||
raw.len() > 300 && !raw.is_char_boundary(300)
|
||||
})
|
||||
.expect("should produce a sample whose byte index 300 is not a char boundary");
|
||||
|
||||
let observer = NoopObserver;
|
||||
let result = execute_one_tool("unknown_tool", call_arguments, &[], &observer, None).await;
|
||||
assert!(result.is_ok(), "execute_one_tool should not panic or error");
|
||||
|
||||
let outcome = result.unwrap();
|
||||
assert!(!outcome.success);
|
||||
assert!(outcome.output.contains("Unknown tool: unknown_tool"));
|
||||
}
|
||||
|
||||
use crate::memory::{Memory, MemoryCategory, SqliteMemory};
|
||||
use crate::observability::NoopObserver;
|
||||
use crate::providers::traits::ProviderCapabilities;
|
||||
@@ -4094,6 +4162,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A tool that always returns a failure with a given error reason.
|
||||
struct FailingTool {
|
||||
tool_name: String,
|
||||
error_reason: String,
|
||||
}
|
||||
|
||||
impl FailingTool {
|
||||
fn new(name: &str, error_reason: &str) -> Self {
|
||||
Self {
|
||||
tool_name: name.to_string(),
|
||||
error_reason: error_reason.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for FailingTool {
|
||||
fn name(&self) -> &str {
|
||||
&self.tool_name
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"A tool that always fails for testing failure surfacing"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": { "type": "string" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_args: serde_json::Value,
|
||||
) -> anyhow::Result<crate::tools::ToolResult> {
|
||||
Ok(crate::tools::ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(self.error_reason.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_tool_call_loop_returns_structured_error_for_non_vision_provider() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
@@ -6434,4 +6548,97 @@ Let me check the result."#;
|
||||
let result = filter_tool_specs_for_turn(specs, &groups, "BROWSE the site");
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
// ── Token-based compaction tests ──────────────────────────
|
||||
|
||||
#[test]
|
||||
fn estimate_history_tokens_empty() {
|
||||
assert_eq!(super::estimate_history_tokens(&[]), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn estimate_history_tokens_single_message() {
|
||||
let history = vec![ChatMessage::user("hello world")]; // 11 chars
|
||||
let tokens = super::estimate_history_tokens(&history);
|
||||
// 11.div_ceil(4) + 4 = 3 + 4 = 7
|
||||
assert_eq!(tokens, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn estimate_history_tokens_multiple_messages() {
|
||||
let history = vec![
|
||||
ChatMessage::system("You are helpful."), // 16 chars → 4 + 4 = 8
|
||||
ChatMessage::user("What is Rust?"), // 13 chars → 4 + 4 = 8
|
||||
ChatMessage::assistant("A language."), // 11 chars → 3 + 4 = 7
|
||||
];
|
||||
let tokens = super::estimate_history_tokens(&history);
|
||||
assert_eq!(tokens, 23);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_tool_call_loop_surfaces_tool_failure_reason_in_on_delta() {
|
||||
let provider = ScriptedProvider::from_text_responses(vec![
|
||||
r#"<tool_call>
|
||||
{"name":"failing_shell","arguments":{"command":"rm -rf /"}}
|
||||
</tool_call>"#,
|
||||
"I could not execute that command.",
|
||||
]);
|
||||
|
||||
let tools_registry: Vec<Box<dyn Tool>> = vec![Box::new(FailingTool::new(
|
||||
"failing_shell",
|
||||
"Command not allowed by security policy: rm -rf /",
|
||||
))];
|
||||
|
||||
let mut history = vec![
|
||||
ChatMessage::system("test-system"),
|
||||
ChatMessage::user("delete everything"),
|
||||
];
|
||||
let observer = NoopObserver;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(64);
|
||||
|
||||
let result = run_tool_call_loop(
|
||||
&provider,
|
||||
&mut history,
|
||||
&tools_registry,
|
||||
&observer,
|
||||
"mock-provider",
|
||||
"mock-model",
|
||||
0.0,
|
||||
true,
|
||||
None,
|
||||
"telegram",
|
||||
&crate::config::MultimodalConfig::default(),
|
||||
4,
|
||||
None,
|
||||
Some(tx),
|
||||
None,
|
||||
&[],
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.expect("tool loop should complete");
|
||||
|
||||
// Collect all messages sent to the on_delta channel.
|
||||
let mut deltas = Vec::new();
|
||||
while let Ok(msg) = rx.try_recv() {
|
||||
deltas.push(msg);
|
||||
}
|
||||
|
||||
let all_deltas = deltas.join("");
|
||||
|
||||
// The failure reason should appear in the progress messages.
|
||||
assert!(
|
||||
all_deltas.contains("Command not allowed by security policy"),
|
||||
"on_delta messages should include the tool failure reason, got: {all_deltas}"
|
||||
);
|
||||
|
||||
// Should also contain the cross mark (❌) icon to indicate failure.
|
||||
assert!(
|
||||
all_deltas.contains('\u{274c}'),
|
||||
"on_delta messages should include ❌ for failed tool calls, got: {all_deltas}"
|
||||
);
|
||||
|
||||
assert_eq!(result, "I could not execute that command.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ impl SystemPromptBuilder {
|
||||
Box::new(WorkspaceSection),
|
||||
Box::new(DateTimeSection),
|
||||
Box::new(RuntimeSection),
|
||||
Box::new(ChannelMediaSection),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -70,6 +71,7 @@ pub struct SkillsSection;
|
||||
pub struct WorkspaceSection;
|
||||
pub struct RuntimeSection;
|
||||
pub struct DateTimeSection;
|
||||
pub struct ChannelMediaSection;
|
||||
|
||||
impl PromptSection for IdentitySection {
|
||||
fn name(&self) -> &str {
|
||||
@@ -206,6 +208,21 @@ impl PromptSection for DateTimeSection {
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptSection for ChannelMediaSection {
|
||||
fn name(&self) -> &str {
|
||||
"channel_media"
|
||||
}
|
||||
|
||||
fn build(&self, _ctx: &PromptContext<'_>) -> Result<String> {
|
||||
Ok("## Channel Media Markers\n\n\
|
||||
Messages from channels may contain media markers:\n\
|
||||
- `[Voice] <text>` — The user sent a voice/audio message that has already been transcribed to text. Respond to the transcribed content directly.\n\
|
||||
- `[IMAGE:<path>]` — An image attachment, processed by the vision pipeline.\n\
|
||||
- `[Document: <name>] <path>` — A file attachment saved to the workspace."
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_workspace_file(prompt: &mut String, workspace_dir: &Path, filename: &str) {
|
||||
let path = workspace_dir.join(filename);
|
||||
match std::fs::read_to_string(&path) {
|
||||
|
||||
+128
-4
@@ -44,11 +44,18 @@ pub struct ApprovalLogEntry {
|
||||
|
||||
// ── ApprovalManager ──────────────────────────────────────────────
|
||||
|
||||
/// Manages the interactive approval workflow.
|
||||
/// Manages the approval workflow for tool calls.
|
||||
///
|
||||
/// - Checks config-level `auto_approve` / `always_ask` lists
|
||||
/// - Maintains a session-scoped "always" allowlist
|
||||
/// - Records an audit trail of all decisions
|
||||
///
|
||||
/// Two modes:
|
||||
/// - **Interactive** (CLI): tools needing approval trigger a stdin prompt.
|
||||
/// - **Non-interactive** (channels): tools needing approval are auto-denied
|
||||
/// because there is no interactive operator to approve them. `auto_approve`
|
||||
/// policy is still enforced, and `always_ask` / supervised-default tools are
|
||||
/// denied rather than silently allowed.
|
||||
pub struct ApprovalManager {
|
||||
/// Tools that never need approval (from config).
|
||||
auto_approve: HashSet<String>,
|
||||
@@ -56,6 +63,9 @@ pub struct ApprovalManager {
|
||||
always_ask: HashSet<String>,
|
||||
/// Autonomy level from config.
|
||||
autonomy_level: AutonomyLevel,
|
||||
/// When `true`, tools that would require interactive approval are
|
||||
/// auto-denied instead. Used for channel-driven (non-CLI) runs.
|
||||
non_interactive: bool,
|
||||
/// Session-scoped allowlist built from "Always" responses.
|
||||
session_allowlist: Mutex<HashSet<String>>,
|
||||
/// Audit trail of approval decisions.
|
||||
@@ -63,17 +73,40 @@ pub struct ApprovalManager {
|
||||
}
|
||||
|
||||
impl ApprovalManager {
|
||||
/// Create from autonomy config.
|
||||
/// Create an interactive (CLI) approval manager from autonomy config.
|
||||
pub fn from_config(config: &AutonomyConfig) -> Self {
|
||||
Self {
|
||||
auto_approve: config.auto_approve.iter().cloned().collect(),
|
||||
always_ask: config.always_ask.iter().cloned().collect(),
|
||||
autonomy_level: config.level,
|
||||
non_interactive: false,
|
||||
session_allowlist: Mutex::new(HashSet::new()),
|
||||
audit_log: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a non-interactive approval manager for channel-driven runs.
|
||||
///
|
||||
/// Enforces the same `auto_approve` / `always_ask` / supervised policies
|
||||
/// as the CLI manager, but tools that would require interactive approval
|
||||
/// are auto-denied instead of prompting (since there is no operator).
|
||||
pub fn for_non_interactive(config: &AutonomyConfig) -> Self {
|
||||
Self {
|
||||
auto_approve: config.auto_approve.iter().cloned().collect(),
|
||||
always_ask: config.always_ask.iter().cloned().collect(),
|
||||
autonomy_level: config.level,
|
||||
non_interactive: true,
|
||||
session_allowlist: Mutex::new(HashSet::new()),
|
||||
audit_log: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` when this manager operates in non-interactive mode
|
||||
/// (i.e. for channel-driven runs where no operator can approve).
|
||||
pub fn is_non_interactive(&self) -> bool {
|
||||
self.non_interactive
|
||||
}
|
||||
|
||||
/// Check whether a tool call requires interactive approval.
|
||||
///
|
||||
/// Returns `true` if the call needs a prompt, `false` if it can proceed.
|
||||
@@ -147,8 +180,8 @@ impl ApprovalManager {
|
||||
|
||||
/// Prompt the user on the CLI and return their decision.
|
||||
///
|
||||
/// For non-CLI channels, returns `Yes` automatically (interactive
|
||||
/// approval is only supported on CLI for now).
|
||||
/// Only called for interactive (CLI) managers. Non-interactive managers
|
||||
/// auto-deny in the tool-call loop before reaching this point.
|
||||
pub fn prompt_cli(&self, request: &ApprovalRequest) -> ApprovalResponse {
|
||||
prompt_cli_interactive(request)
|
||||
}
|
||||
@@ -401,6 +434,97 @@ mod tests {
|
||||
assert!(summary.contains("just a string"));
|
||||
}
|
||||
|
||||
// ── non-interactive (channel) mode ────────────────────────
|
||||
|
||||
#[test]
|
||||
fn non_interactive_manager_reports_non_interactive() {
|
||||
let mgr = ApprovalManager::for_non_interactive(&supervised_config());
|
||||
assert!(mgr.is_non_interactive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_manager_reports_interactive() {
|
||||
let mgr = ApprovalManager::from_config(&supervised_config());
|
||||
assert!(!mgr.is_non_interactive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_auto_approve_tools_skip_approval() {
|
||||
let mgr = ApprovalManager::for_non_interactive(&supervised_config());
|
||||
// auto_approve tools (file_read, memory_recall) should not need approval.
|
||||
assert!(!mgr.needs_approval("file_read"));
|
||||
assert!(!mgr.needs_approval("memory_recall"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_always_ask_tools_need_approval() {
|
||||
let mgr = ApprovalManager::for_non_interactive(&supervised_config());
|
||||
// always_ask tools (shell) still report as needing approval,
|
||||
// so the tool-call loop will auto-deny them in non-interactive mode.
|
||||
assert!(mgr.needs_approval("shell"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_unknown_tools_need_approval_in_supervised() {
|
||||
let mgr = ApprovalManager::for_non_interactive(&supervised_config());
|
||||
// Unknown tools in supervised mode need approval (will be auto-denied
|
||||
// by the tool-call loop for non-interactive managers).
|
||||
assert!(mgr.needs_approval("file_write"));
|
||||
assert!(mgr.needs_approval("http_request"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_full_autonomy_never_needs_approval() {
|
||||
let mgr = ApprovalManager::for_non_interactive(&full_config());
|
||||
// Full autonomy means no approval needed, even in non-interactive mode.
|
||||
assert!(!mgr.needs_approval("shell"));
|
||||
assert!(!mgr.needs_approval("file_write"));
|
||||
assert!(!mgr.needs_approval("anything"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_readonly_never_needs_approval() {
|
||||
let config = AutonomyConfig {
|
||||
level: AutonomyLevel::ReadOnly,
|
||||
..AutonomyConfig::default()
|
||||
};
|
||||
let mgr = ApprovalManager::for_non_interactive(&config);
|
||||
// ReadOnly blocks execution elsewhere; approval manager does not prompt.
|
||||
assert!(!mgr.needs_approval("shell"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_session_allowlist_still_works() {
|
||||
let mgr = ApprovalManager::for_non_interactive(&supervised_config());
|
||||
assert!(mgr.needs_approval("file_write"));
|
||||
|
||||
// Simulate an "Always" decision (would come from a prior channel run
|
||||
// if the tool was auto-approved somehow, e.g. via config change).
|
||||
mgr.record_decision(
|
||||
"file_write",
|
||||
&serde_json::json!({"path": "test.txt"}),
|
||||
ApprovalResponse::Always,
|
||||
"telegram",
|
||||
);
|
||||
|
||||
assert!(!mgr.needs_approval("file_write"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interactive_always_ask_overrides_session_allowlist() {
|
||||
let mgr = ApprovalManager::for_non_interactive(&supervised_config());
|
||||
|
||||
mgr.record_decision(
|
||||
"shell",
|
||||
&serde_json::json!({"command": "ls"}),
|
||||
ApprovalResponse::Always,
|
||||
"telegram",
|
||||
);
|
||||
|
||||
// shell is in always_ask, so it still needs approval even after "Always".
|
||||
assert!(mgr.needs_approval("shell"));
|
||||
}
|
||||
|
||||
// ── ApprovalResponse serde ───────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
+122
-58
@@ -59,6 +59,54 @@ impl LinqChannel {
|
||||
Some(format!("[IMAGE:{source}]"))
|
||||
}
|
||||
|
||||
fn sender_is_from_me(data: &serde_json::Value) -> bool {
|
||||
// Legacy format: data.is_from_me
|
||||
if let Some(v) = data.get("is_from_me").and_then(|value| value.as_bool()) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// New format: data.sender_handle.is_me OR data.direction == "outbound"
|
||||
let is_me = data
|
||||
.get("sender_handle")
|
||||
.and_then(|value| value.get("is_me"))
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let is_outbound = matches!(
|
||||
data.get("direction").and_then(|value| value.as_str()),
|
||||
Some("outbound")
|
||||
);
|
||||
|
||||
is_me || is_outbound
|
||||
}
|
||||
|
||||
fn sender_handle(data: &serde_json::Value) -> Option<&str> {
|
||||
data.get("from")
|
||||
.and_then(|value| value.as_str())
|
||||
.or_else(|| {
|
||||
data.get("sender_handle")
|
||||
.and_then(|value| value.get("handle"))
|
||||
.and_then(|value| value.as_str())
|
||||
})
|
||||
}
|
||||
|
||||
fn chat_id(data: &serde_json::Value) -> Option<&str> {
|
||||
data.get("chat_id")
|
||||
.and_then(|value| value.as_str())
|
||||
.or_else(|| {
|
||||
data.get("chat")
|
||||
.and_then(|value| value.get("id"))
|
||||
.and_then(|value| value.as_str())
|
||||
})
|
||||
}
|
||||
|
||||
fn message_parts(data: &serde_json::Value) -> Option<&Vec<serde_json::Value>> {
|
||||
data.get("message")
|
||||
.and_then(|value| value.get("parts"))
|
||||
.and_then(|value| value.as_array())
|
||||
.or_else(|| data.get("parts").and_then(|value| value.as_array()))
|
||||
}
|
||||
|
||||
/// Parse an incoming webhook payload from Linq and extract messages.
|
||||
///
|
||||
/// Supports two webhook formats:
|
||||
@@ -95,6 +143,11 @@ impl LinqChannel {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Also accepts the current 2026-02-03 payload shape where `chat_id`,
|
||||
/// `from`, `is_from_me`, and `message.parts` moved under:
|
||||
/// `data.chat.id`, `data.sender_handle.handle`, `data.sender_handle.is_me`,
|
||||
/// and `data.parts`.
|
||||
pub fn parse_webhook_payload(&self, payload: &serde_json::Value) -> Vec<ChannelMessage> {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
@@ -112,44 +165,14 @@ impl LinqChannel {
|
||||
return messages;
|
||||
};
|
||||
|
||||
// Detect format: new format has `sender_handle`, legacy has `from`.
|
||||
let is_new_format = data.get("sender_handle").is_some();
|
||||
|
||||
// Skip messages sent by the bot itself
|
||||
let is_from_me = if is_new_format {
|
||||
// New format: data.sender_handle.is_me or data.direction == "outbound"
|
||||
data.get("sender_handle")
|
||||
.and_then(|sh| sh.get("is_me"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
|| data
|
||||
.get("direction")
|
||||
.and_then(|d| d.as_str())
|
||||
.is_some_and(|d| d == "outbound")
|
||||
} else {
|
||||
// Legacy format: data.is_from_me
|
||||
data.get("is_from_me")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if is_from_me {
|
||||
if Self::sender_is_from_me(data) {
|
||||
tracing::debug!("Linq: skipping is_from_me message");
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Get sender phone number
|
||||
let from = if is_new_format {
|
||||
// New format: data.sender_handle.handle
|
||||
data.get("sender_handle")
|
||||
.and_then(|sh| sh.get("handle"))
|
||||
.and_then(|h| h.as_str())
|
||||
} else {
|
||||
// Legacy format: data.from
|
||||
data.get("from").and_then(|f| f.as_str())
|
||||
};
|
||||
|
||||
let Some(from) = from else {
|
||||
let Some(from) = Self::sender_handle(data) else {
|
||||
return messages;
|
||||
};
|
||||
|
||||
@@ -171,33 +194,10 @@ impl LinqChannel {
|
||||
}
|
||||
|
||||
// Get chat_id for reply routing
|
||||
let chat_id = if is_new_format {
|
||||
// New format: data.chat.id
|
||||
data.get("chat")
|
||||
.and_then(|c| c.get("id"))
|
||||
.and_then(|id| id.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
} else {
|
||||
// Legacy format: data.chat_id
|
||||
data.get("chat_id")
|
||||
.and_then(|c| c.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
};
|
||||
let chat_id = Self::chat_id(data).unwrap_or("").to_string();
|
||||
|
||||
// Extract message parts
|
||||
let parts = if is_new_format {
|
||||
// New format: data.parts (directly on data)
|
||||
data.get("parts").and_then(|p| p.as_array())
|
||||
} else {
|
||||
// Legacy format: data.message.parts
|
||||
data.get("message")
|
||||
.and_then(|m| m.get("parts"))
|
||||
.and_then(|p| p.as_array())
|
||||
};
|
||||
|
||||
let Some(parts) = parts else {
|
||||
// Extract text from message parts
|
||||
let Some(parts) = Self::message_parts(data) else {
|
||||
return messages;
|
||||
};
|
||||
|
||||
@@ -520,6 +520,42 @@ mod tests {
|
||||
assert_eq!(msgs[0].reply_target, "chat-789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linq_parse_latest_webhook_shape() {
|
||||
let ch = LinqChannel::new(
|
||||
"tok".into(),
|
||||
"+15551234567".into(),
|
||||
vec!["+1234567890".into()],
|
||||
);
|
||||
let payload = serde_json::json!({
|
||||
"api_version": "v3",
|
||||
"webhook_version": "2026-02-03",
|
||||
"event_type": "message.received",
|
||||
"created_at": "2026-02-03T12:00:00Z",
|
||||
"data": {
|
||||
"chat": {
|
||||
"id": "chat-2026"
|
||||
},
|
||||
"direction": "inbound",
|
||||
"id": "msg-2026",
|
||||
"parts": [{
|
||||
"type": "text",
|
||||
"value": "Hello from the latest payload"
|
||||
}],
|
||||
"sender_handle": {
|
||||
"handle": "1234567890",
|
||||
"is_me": false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let msgs = ch.parse_webhook_payload(&payload);
|
||||
assert_eq!(msgs.len(), 1);
|
||||
assert_eq!(msgs[0].sender, "+1234567890");
|
||||
assert_eq!(msgs[0].content, "Hello from the latest payload");
|
||||
assert_eq!(msgs[0].reply_target, "chat-2026");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linq_parse_skip_is_from_me() {
|
||||
let ch = LinqChannel::new("tok".into(), "+15551234567".into(), vec!["*".into()]);
|
||||
@@ -540,6 +576,34 @@ mod tests {
|
||||
assert!(msgs.is_empty(), "is_from_me messages should be skipped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linq_parse_skip_latest_outbound_message() {
|
||||
let ch = LinqChannel::new("tok".into(), "+15551234567".into(), vec!["*".into()]);
|
||||
let payload = serde_json::json!({
|
||||
"event_type": "message.received",
|
||||
"data": {
|
||||
"chat": {
|
||||
"id": "chat-789"
|
||||
},
|
||||
"direction": "outbound",
|
||||
"parts": [{
|
||||
"type": "text",
|
||||
"value": "My own message"
|
||||
}],
|
||||
"sender_handle": {
|
||||
"handle": "+1234567890",
|
||||
"is_me": true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let msgs = ch.parse_webhook_payload(&payload);
|
||||
assert!(
|
||||
msgs.is_empty(),
|
||||
"latest outbound messages from the bot should be skipped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linq_parse_skip_non_message_event() {
|
||||
let ch = make_channel();
|
||||
|
||||
@@ -746,7 +746,7 @@ impl Channel for MatrixChannel {
|
||||
MessageType::Notice(content) => (content.body.clone(), None),
|
||||
MessageType::Image(content) => {
|
||||
let dl = media_info(&content.source, &content.body);
|
||||
(format!("[image: {}]", content.body), dl)
|
||||
(format!("[IMAGE:{}]", content.body), dl)
|
||||
}
|
||||
MessageType::File(content) => {
|
||||
let dl = media_info(&content.source, &content.body);
|
||||
@@ -888,7 +888,7 @@ impl Channel for MatrixChannel {
|
||||
sender: sender.clone(),
|
||||
reply_target: format!("{}||{}", sender, room.room_id()),
|
||||
content: body,
|
||||
channel: format!("matrix:{}", room.room_id()),
|
||||
channel: "matrix".to_string(),
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
|
||||
+620
-45
File diff suppressed because it is too large
Load Diff
@@ -63,14 +63,18 @@ impl NextcloudTalkChannel {
|
||||
/// Parse a Nextcloud Talk webhook payload into channel messages.
|
||||
///
|
||||
/// Relevant payload fields:
|
||||
/// - `type` (expects `message`)
|
||||
/// - `type` (accepts `message` or `Create`)
|
||||
/// - `object.token` (room token for reply routing)
|
||||
/// - `message.actorType`, `message.actorId`, `message.message`, `message.timestamp`
|
||||
pub fn parse_webhook_payload(&self, payload: &serde_json::Value) -> Vec<ChannelMessage> {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
if let Some(event_type) = payload.get("type").and_then(|v| v.as_str()) {
|
||||
if !event_type.eq_ignore_ascii_case("message") {
|
||||
// Nextcloud Talk bot webhooks send "Create" for new chat messages,
|
||||
// but some setups may use "message". Accept both.
|
||||
let is_message_event = event_type.eq_ignore_ascii_case("message")
|
||||
|| event_type.eq_ignore_ascii_case("create");
|
||||
if !is_message_event {
|
||||
tracing::debug!("Nextcloud Talk: skipping non-message event: {event_type}");
|
||||
return messages;
|
||||
}
|
||||
@@ -338,6 +342,36 @@ mod tests {
|
||||
assert_eq!(messages[0].timestamp, 1_735_701_200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nextcloud_talk_parse_create_event_type() {
|
||||
let channel = make_channel();
|
||||
let payload = serde_json::json!({
|
||||
"type": "Create",
|
||||
"object": {
|
||||
"id": "42",
|
||||
"token": "room-token-123",
|
||||
"name": "Team Room",
|
||||
"type": "room"
|
||||
},
|
||||
"message": {
|
||||
"id": 88,
|
||||
"token": "room-token-123",
|
||||
"actorType": "users",
|
||||
"actorId": "user_a",
|
||||
"actorDisplayName": "User A",
|
||||
"timestamp": 1_735_701_300,
|
||||
"messageType": "comment",
|
||||
"systemMessage": "",
|
||||
"message": "Hello via Create event"
|
||||
}
|
||||
});
|
||||
|
||||
let messages = channel.parse_webhook_payload(&payload);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].id, "88");
|
||||
assert_eq!(messages[0].content, "Hello via Create event");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nextcloud_talk_parse_skips_non_message_events() {
|
||||
let channel = make_channel();
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
//! JSONL-based session persistence for channel conversations.
|
||||
//!
|
||||
//! Each session (keyed by `channel_sender` or `channel_thread_sender`) is stored
|
||||
//! as an append-only JSONL file in `{workspace}/sessions/`. Messages are appended
|
||||
//! one-per-line as JSON, never modifying old lines. On daemon restart, sessions
|
||||
//! are loaded from disk to restore conversation context.
|
||||
|
||||
use crate::providers::traits::ChatMessage;
|
||||
use std::io::{BufRead, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Append-only JSONL session store for channel conversations.
|
||||
pub struct SessionStore {
|
||||
sessions_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl SessionStore {
|
||||
/// Create a new session store, ensuring the sessions directory exists.
|
||||
pub fn new(workspace_dir: &Path) -> std::io::Result<Self> {
|
||||
let sessions_dir = workspace_dir.join("sessions");
|
||||
std::fs::create_dir_all(&sessions_dir)?;
|
||||
Ok(Self { sessions_dir })
|
||||
}
|
||||
|
||||
/// Compute the file path for a session key, sanitizing for filesystem safety.
|
||||
fn session_path(&self, session_key: &str) -> PathBuf {
|
||||
let safe_key: String = session_key
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_alphanumeric() || c == '_' || c == '-' {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
self.sessions_dir.join(format!("{safe_key}.jsonl"))
|
||||
}
|
||||
|
||||
/// Load all messages for a session from its JSONL file.
|
||||
/// Returns an empty vec if the file does not exist or is unreadable.
|
||||
pub fn load(&self, session_key: &str) -> Vec<ChatMessage> {
|
||||
let path = self.session_path(session_key);
|
||||
let file = match std::fs::File::open(&path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let mut messages = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let Ok(line) = line else { continue };
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(msg) = serde_json::from_str::<ChatMessage>(trimmed) {
|
||||
messages.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
messages
|
||||
}
|
||||
|
||||
/// Append a single message to the session JSONL file.
|
||||
pub fn append(&self, session_key: &str, message: &ChatMessage) -> std::io::Result<()> {
|
||||
let path = self.session_path(session_key);
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)?;
|
||||
|
||||
let json = serde_json::to_string(message)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
writeln!(file, "{json}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all session keys that have files on disk.
|
||||
pub fn list_sessions(&self) -> Vec<String> {
|
||||
let entries = match std::fs::read_dir(&self.sessions_dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
entries
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let name = entry.file_name().into_string().ok()?;
|
||||
name.strip_suffix(".jsonl").map(String::from)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn round_trip_append_and_load() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = SessionStore::new(tmp.path()).unwrap();
|
||||
|
||||
store
|
||||
.append("telegram_user123", &ChatMessage::user("hello"))
|
||||
.unwrap();
|
||||
store
|
||||
.append("telegram_user123", &ChatMessage::assistant("hi there"))
|
||||
.unwrap();
|
||||
|
||||
let messages = store.load("telegram_user123");
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert_eq!(messages[0].role, "user");
|
||||
assert_eq!(messages[0].content, "hello");
|
||||
assert_eq!(messages[1].role, "assistant");
|
||||
assert_eq!(messages[1].content, "hi there");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_nonexistent_session_returns_empty() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = SessionStore::new(tmp.path()).unwrap();
|
||||
|
||||
let messages = store.load("nonexistent");
|
||||
assert!(messages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_sanitization() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = SessionStore::new(tmp.path()).unwrap();
|
||||
|
||||
// Keys with special chars should be sanitized
|
||||
store
|
||||
.append("slack/thread:123/user", &ChatMessage::user("test"))
|
||||
.unwrap();
|
||||
|
||||
let messages = store.load("slack/thread:123/user");
|
||||
assert_eq!(messages.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_sessions_returns_keys() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = SessionStore::new(tmp.path()).unwrap();
|
||||
|
||||
store
|
||||
.append("telegram_alice", &ChatMessage::user("hi"))
|
||||
.unwrap();
|
||||
store
|
||||
.append("discord_bob", &ChatMessage::user("hey"))
|
||||
.unwrap();
|
||||
|
||||
let mut sessions = store.list_sessions();
|
||||
sessions.sort();
|
||||
assert_eq!(sessions.len(), 2);
|
||||
assert!(sessions.contains(&"discord_bob".to_string()));
|
||||
assert!(sessions.contains(&"telegram_alice".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_is_truly_append_only() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = SessionStore::new(tmp.path()).unwrap();
|
||||
let key = "test_session";
|
||||
|
||||
store.append(key, &ChatMessage::user("msg1")).unwrap();
|
||||
store.append(key, &ChatMessage::user("msg2")).unwrap();
|
||||
|
||||
// Read raw file to verify append-only format
|
||||
let path = store.session_path(key);
|
||||
let content = std::fs::read_to_string(&path).unwrap();
|
||||
let lines: Vec<&str> = content.trim().lines().collect();
|
||||
assert_eq!(lines.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_corrupt_lines_gracefully() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = SessionStore::new(tmp.path()).unwrap();
|
||||
let key = "corrupt_test";
|
||||
|
||||
// Write valid message + corrupt line + valid message
|
||||
let path = store.session_path(key);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let mut file = std::fs::File::create(&path).unwrap();
|
||||
writeln!(file, r#"{{"role":"user","content":"hello"}}"#).unwrap();
|
||||
writeln!(file, "this is not valid json").unwrap();
|
||||
writeln!(file, r#"{{"role":"assistant","content":"world"}}"#).unwrap();
|
||||
|
||||
let messages = store.load(key);
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert_eq!(messages[0].content, "hello");
|
||||
assert_eq!(messages[1].content, "world");
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,9 @@ fn format_attachment_content(
|
||||
local_path: &Path,
|
||||
) -> String {
|
||||
match kind {
|
||||
IncomingAttachmentKind::Photo if is_image_extension(local_path) => {
|
||||
IncomingAttachmentKind::Photo | IncomingAttachmentKind::Document
|
||||
if is_image_extension(local_path) =>
|
||||
{
|
||||
format!("[IMAGE:{}]", local_path.display())
|
||||
}
|
||||
_ => {
|
||||
@@ -246,6 +248,23 @@ fn strip_tool_call_tags(message: &str) -> String {
|
||||
super::strip_tool_call_tags(message)
|
||||
}
|
||||
|
||||
fn find_matching_close(s: &str) -> Option<usize> {
|
||||
let mut depth = 1usize;
|
||||
for (i, ch) in s.char_indices() {
|
||||
match ch {
|
||||
'[' => depth += 1,
|
||||
']' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_attachment_markers(message: &str) -> (String, Vec<TelegramAttachment>) {
|
||||
let mut cleaned = String::with_capacity(message.len());
|
||||
let mut attachments = Vec::new();
|
||||
@@ -260,12 +279,12 @@ fn parse_attachment_markers(message: &str) -> (String, Vec<TelegramAttachment>)
|
||||
let open = cursor + open_rel;
|
||||
cleaned.push_str(&message[cursor..open]);
|
||||
|
||||
let Some(close_rel) = message[open..].find(']') else {
|
||||
let Some(close_rel) = find_matching_close(&message[open + 1..]) else {
|
||||
cleaned.push_str(&message[open..]);
|
||||
break;
|
||||
};
|
||||
|
||||
let close = open + close_rel;
|
||||
let close = open + 1 + close_rel;
|
||||
let marker = &message[open + 1..close];
|
||||
|
||||
let parsed = marker.split_once(':').and_then(|(kind, target)| {
|
||||
|
||||
+3
-2
@@ -17,8 +17,9 @@ pub use schema::{
|
||||
QueryClassificationConfig, ReliabilityConfig, ResourceLimitsConfig, RuntimeConfig,
|
||||
SandboxBackend, SandboxConfig, SchedulerConfig, SecretsConfig, SecurityConfig, SkillsConfig,
|
||||
SkillsPromptInjectionMode, SlackConfig, StorageConfig, StorageProviderConfig,
|
||||
StorageProviderSection, StreamMode, TelegramConfig, ToolFilterGroup, ToolFilterGroupMode,
|
||||
TranscriptionConfig, TtsConfig, TunnelConfig, WebFetchConfig, WebSearchConfig, WebhookConfig,
|
||||
StorageProviderSection, StreamMode, SwarmConfig, SwarmStrategy, TelegramConfig,
|
||||
ToolFilterGroup, ToolFilterGroupMode, TranscriptionConfig, TtsConfig, TunnelConfig,
|
||||
WebFetchConfig, WebSearchConfig, WebhookConfig,
|
||||
};
|
||||
|
||||
pub fn name_and_presence<T: traits::ChannelConfig>(channel: Option<&T>) -> (&'static str, bool) {
|
||||
|
||||
+357
-14
@@ -232,6 +232,10 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub agents: HashMap<String, DelegateAgentConfig>,
|
||||
|
||||
/// Swarm configurations for multi-agent orchestration.
|
||||
#[serde(default)]
|
||||
pub swarms: HashMap<String, SwarmConfig>,
|
||||
|
||||
/// Hooks configuration (lifecycle hooks and built-in hook toggles).
|
||||
#[serde(default)]
|
||||
pub hooks: HooksConfig,
|
||||
@@ -319,6 +323,44 @@ pub struct DelegateAgentConfig {
|
||||
pub max_iterations: usize,
|
||||
}
|
||||
|
||||
// ── Swarms ──────────────────────────────────────────────────────
|
||||
|
||||
/// Orchestration strategy for a swarm of agents.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SwarmStrategy {
|
||||
/// Run agents sequentially; each agent's output feeds into the next.
|
||||
Sequential,
|
||||
/// Run agents in parallel; collect all outputs.
|
||||
Parallel,
|
||||
/// Use the LLM to pick the best agent for the task.
|
||||
Router,
|
||||
}
|
||||
|
||||
/// Configuration for a swarm of coordinated agents.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SwarmConfig {
|
||||
/// Ordered list of agent names (must reference keys in `agents`).
|
||||
pub agents: Vec<String>,
|
||||
/// Orchestration strategy.
|
||||
pub strategy: SwarmStrategy,
|
||||
/// System prompt for router strategy (used to pick the best agent).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub router_prompt: Option<String>,
|
||||
/// Optional description shown to the LLM when choosing swarms.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Maximum total timeout for the swarm execution in seconds.
|
||||
#[serde(default = "default_swarm_timeout_secs")]
|
||||
pub timeout_secs: u64,
|
||||
}
|
||||
|
||||
const DEFAULT_SWARM_TIMEOUT_SECS: u64 = 300;
|
||||
|
||||
fn default_swarm_timeout_secs() -> u64 {
|
||||
DEFAULT_SWARM_TIMEOUT_SECS
|
||||
}
|
||||
|
||||
/// Valid temperature range for all paths (config, CLI, env override).
|
||||
pub const TEMPERATURE_RANGE: std::ops::RangeInclusive<f64> = 0.0..=2.0;
|
||||
|
||||
@@ -791,6 +833,11 @@ pub struct AgentConfig {
|
||||
/// Maximum conversation history messages retained per session. Default: `50`.
|
||||
#[serde(default = "default_agent_max_history_messages")]
|
||||
pub max_history_messages: usize,
|
||||
/// Maximum estimated tokens for conversation history before compaction triggers.
|
||||
/// Uses ~4 chars/token heuristic. When this threshold is exceeded, older messages
|
||||
/// are summarized to preserve context while staying within budget. Default: `32000`.
|
||||
#[serde(default = "default_agent_max_context_tokens")]
|
||||
pub max_context_tokens: usize,
|
||||
/// Enable parallel tool execution within a single iteration. Default: `false`.
|
||||
#[serde(default)]
|
||||
pub parallel_tools: bool,
|
||||
@@ -817,6 +864,10 @@ fn default_agent_max_history_messages() -> usize {
|
||||
50
|
||||
}
|
||||
|
||||
fn default_agent_max_context_tokens() -> usize {
|
||||
32_000
|
||||
}
|
||||
|
||||
fn default_agent_tool_dispatcher() -> String {
|
||||
"auto".into()
|
||||
}
|
||||
@@ -827,6 +878,7 @@ impl Default for AgentConfig {
|
||||
compact_context: false,
|
||||
max_tool_iterations: default_agent_max_tool_iterations(),
|
||||
max_history_messages: default_agent_max_history_messages(),
|
||||
max_context_tokens: default_agent_max_context_tokens(),
|
||||
parallel_tools: false,
|
||||
tool_dispatcher: default_agent_tool_dispatcher(),
|
||||
tool_call_dedup_exempt: Vec::new(),
|
||||
@@ -1413,6 +1465,10 @@ pub struct HttpRequestConfig {
|
||||
/// Request timeout in seconds (default: 30)
|
||||
#[serde(default = "default_http_timeout_secs")]
|
||||
pub timeout_secs: u64,
|
||||
/// Allow requests to private/LAN hosts (RFC 1918, loopback, link-local, .local).
|
||||
/// Default: false (deny private hosts for SSRF protection).
|
||||
#[serde(default)]
|
||||
pub allow_private_hosts: bool,
|
||||
}
|
||||
|
||||
impl Default for HttpRequestConfig {
|
||||
@@ -1422,6 +1478,7 @@ impl Default for HttpRequestConfig {
|
||||
allowed_domains: vec![],
|
||||
max_response_size: default_http_max_response_size(),
|
||||
timeout_secs: default_http_timeout_secs(),
|
||||
allow_private_hosts: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2895,22 +2952,34 @@ pub struct HeartbeatConfig {
|
||||
pub enabled: bool,
|
||||
/// Interval in minutes between heartbeat pings. Default: `30`.
|
||||
pub interval_minutes: u32,
|
||||
/// Enable two-phase heartbeat: Phase 1 asks LLM whether to run, Phase 2
|
||||
/// executes only when the LLM decides there is work to do. Saves API cost
|
||||
/// during quiet periods. Default: `true`.
|
||||
#[serde(default = "default_two_phase")]
|
||||
pub two_phase: bool,
|
||||
/// Optional fallback task text when `HEARTBEAT.md` has no task entries.
|
||||
#[serde(default)]
|
||||
pub message: Option<String>,
|
||||
/// Optional delivery channel for heartbeat output (for example: `telegram`).
|
||||
/// When omitted, auto-selects the first configured channel.
|
||||
#[serde(default, alias = "channel")]
|
||||
pub target: Option<String>,
|
||||
/// Optional delivery recipient/chat identifier (required when `target` is set).
|
||||
/// Optional delivery recipient/chat identifier (required when `target` is
|
||||
/// explicitly set).
|
||||
#[serde(default, alias = "recipient")]
|
||||
pub to: Option<String>,
|
||||
}
|
||||
|
||||
fn default_two_phase() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for HeartbeatConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
interval_minutes: 30,
|
||||
two_phase: true,
|
||||
message: None,
|
||||
target: None,
|
||||
to: None,
|
||||
@@ -3040,6 +3109,7 @@ impl<T: ChannelConfig> crate::config::traits::ConfigHandle for ConfigWrapper<T>
|
||||
///
|
||||
/// Each channel sub-section (e.g. `telegram`, `discord`) is optional;
|
||||
/// setting it to `Some(...)` enables that channel.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ChannelsConfig {
|
||||
/// Enable the CLI interactive channel. Default: `true`.
|
||||
@@ -3097,6 +3167,15 @@ pub struct ChannelsConfig {
|
||||
/// completion) to incoming channel messages. Default: `true`.
|
||||
#[serde(default = "default_true")]
|
||||
pub ack_reactions: bool,
|
||||
/// Whether to send tool-call notification messages (e.g. `🔧 web_search_tool: …`)
|
||||
/// to channel users. When `false`, tool calls are still logged server-side but
|
||||
/// not forwarded as individual channel messages. Default: `true`.
|
||||
#[serde(default = "default_true")]
|
||||
pub show_tool_calls: bool,
|
||||
/// Persist channel conversation history to JSONL files so sessions survive
|
||||
/// daemon restarts. Files are stored in `{workspace}/sessions/`. Default: `true`.
|
||||
#[serde(default = "default_true")]
|
||||
pub session_persistence: bool,
|
||||
}
|
||||
|
||||
impl ChannelsConfig {
|
||||
@@ -3230,6 +3309,8 @@ impl Default for ChannelsConfig {
|
||||
clawdtalk: None,
|
||||
message_timeout_secs: default_channel_message_timeout_secs(),
|
||||
ack_reactions: true,
|
||||
show_tool_calls: true,
|
||||
session_persistence: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3323,6 +3404,10 @@ pub struct SlackConfig {
|
||||
/// Allowed Slack user IDs. Empty = deny all.
|
||||
#[serde(default)]
|
||||
pub allowed_users: Vec<String>,
|
||||
/// When true, a newer Slack message from the same sender in the same channel
|
||||
/// cancels the in-flight request and starts a fresh response with preserved history.
|
||||
#[serde(default)]
|
||||
pub interrupt_on_new_message: bool,
|
||||
}
|
||||
|
||||
impl ChannelConfig for SlackConfig {
|
||||
@@ -4159,6 +4244,7 @@ impl Default for Config {
|
||||
cost: CostConfig::default(),
|
||||
peripherals: PeripheralsConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
swarms: HashMap::new(),
|
||||
hooks: HooksConfig::default(),
|
||||
hardware: HardwareConfig::default(),
|
||||
query_classification: QueryClassificationConfig::default(),
|
||||
@@ -4687,6 +4773,23 @@ impl Config {
|
||||
"config.channels_config.nostr.private_key",
|
||||
)?;
|
||||
}
|
||||
if let Some(ref mut fs) = config.channels_config.feishu {
|
||||
decrypt_secret(
|
||||
&store,
|
||||
&mut fs.app_secret,
|
||||
"config.channels_config.feishu.app_secret",
|
||||
)?;
|
||||
decrypt_optional_secret(
|
||||
&store,
|
||||
&mut fs.encrypt_key,
|
||||
"config.channels_config.feishu.encrypt_key",
|
||||
)?;
|
||||
decrypt_optional_secret(
|
||||
&store,
|
||||
&mut fs.verification_token,
|
||||
"config.channels_config.feishu.verification_token",
|
||||
)?;
|
||||
}
|
||||
|
||||
// Decrypt channel secrets
|
||||
if let Some(ref mut tg) = config.channels_config.telegram {
|
||||
@@ -5509,11 +5612,38 @@ impl Config {
|
||||
set_runtime_proxy_config(self.proxy.clone());
|
||||
}
|
||||
|
||||
async fn resolve_config_path_for_save(&self) -> Result<PathBuf> {
|
||||
if self
|
||||
.config_path
|
||||
.parent()
|
||||
.is_some_and(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
return Ok(self.config_path.clone());
|
||||
}
|
||||
|
||||
let (default_zeroclaw_dir, default_workspace_dir) = default_config_and_workspace_dirs()?;
|
||||
let (zeroclaw_dir, _workspace_dir, source) =
|
||||
resolve_runtime_config_dirs(&default_zeroclaw_dir, &default_workspace_dir).await?;
|
||||
let file_name = self
|
||||
.config_path
|
||||
.file_name()
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new("config.toml"));
|
||||
let resolved = zeroclaw_dir.join(file_name);
|
||||
tracing::warn!(
|
||||
path = %self.config_path.display(),
|
||||
resolved = %resolved.display(),
|
||||
source = source.as_str(),
|
||||
"Config path missing parent directory; resolving from runtime environment"
|
||||
);
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
// Encrypt secrets before serialization
|
||||
let mut config_to_save = self.clone();
|
||||
let zeroclaw_dir = self
|
||||
.config_path
|
||||
let config_path = self.resolve_config_path_for_save().await?;
|
||||
let zeroclaw_dir = config_path
|
||||
.parent()
|
||||
.context("Config path must have a parent directory")?;
|
||||
let store = crate::security::SecretStore::new(zeroclaw_dir, self.secrets.encrypt);
|
||||
@@ -5570,6 +5700,23 @@ impl Config {
|
||||
"config.channels_config.nostr.private_key",
|
||||
)?;
|
||||
}
|
||||
if let Some(ref mut fs) = config_to_save.channels_config.feishu {
|
||||
encrypt_secret(
|
||||
&store,
|
||||
&mut fs.app_secret,
|
||||
"config.channels_config.feishu.app_secret",
|
||||
)?;
|
||||
encrypt_optional_secret(
|
||||
&store,
|
||||
&mut fs.encrypt_key,
|
||||
"config.channels_config.feishu.encrypt_key",
|
||||
)?;
|
||||
encrypt_optional_secret(
|
||||
&store,
|
||||
&mut fs.verification_token,
|
||||
"config.channels_config.feishu.verification_token",
|
||||
)?;
|
||||
}
|
||||
|
||||
// Encrypt channel secrets
|
||||
if let Some(ref mut tg) = config_to_save.channels_config.telegram {
|
||||
@@ -5767,8 +5914,7 @@ impl Config {
|
||||
let toml_str =
|
||||
toml::to_string_pretty(&config_to_save).context("Failed to serialize config")?;
|
||||
|
||||
let parent_dir = self
|
||||
.config_path
|
||||
let parent_dir = config_path
|
||||
.parent()
|
||||
.context("Config path must have a parent directory")?;
|
||||
|
||||
@@ -5779,8 +5925,7 @@ impl Config {
|
||||
)
|
||||
})?;
|
||||
|
||||
let file_name = self
|
||||
.config_path
|
||||
let file_name = config_path
|
||||
.file_name()
|
||||
.and_then(|v| v.to_str())
|
||||
.unwrap_or("config.toml");
|
||||
@@ -5808,9 +5953,9 @@ impl Config {
|
||||
.context("Failed to fsync temporary config file")?;
|
||||
drop(temp_file);
|
||||
|
||||
let had_existing_config = self.config_path.exists();
|
||||
let had_existing_config = config_path.exists();
|
||||
if had_existing_config {
|
||||
fs::copy(&self.config_path, &backup_path)
|
||||
fs::copy(&config_path, &backup_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
@@ -5820,10 +5965,10 @@ impl Config {
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Err(e) = fs::rename(&temp_path, &self.config_path).await {
|
||||
if let Err(e) = fs::rename(&temp_path, &config_path).await {
|
||||
let _ = fs::remove_file(&temp_path).await;
|
||||
if had_existing_config && backup_path.exists() {
|
||||
fs::copy(&backup_path, &self.config_path)
|
||||
fs::copy(&backup_path, &config_path)
|
||||
.await
|
||||
.context("Failed to restore config backup")?;
|
||||
}
|
||||
@@ -5833,12 +5978,11 @@ impl Config {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
|
||||
if let Err(err) =
|
||||
fs::set_permissions(&self.config_path, Permissions::from_mode(0o600)).await
|
||||
if let Err(err) = fs::set_permissions(&config_path, Permissions::from_mode(0o600)).await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to harden config permissions to 0600 at {}: {}",
|
||||
self.config_path.display(),
|
||||
config_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
@@ -6149,6 +6293,7 @@ default_temperature = 0.7
|
||||
heartbeat: HeartbeatConfig {
|
||||
enabled: true,
|
||||
interval_minutes: 15,
|
||||
two_phase: true,
|
||||
message: Some("Check London time".into()),
|
||||
target: Some("telegram".into()),
|
||||
to: Some("123456".into()),
|
||||
@@ -6187,6 +6332,8 @@ default_temperature = 0.7
|
||||
clawdtalk: None,
|
||||
message_timeout_secs: 300,
|
||||
ack_reactions: true,
|
||||
show_tool_calls: true,
|
||||
session_persistence: true,
|
||||
},
|
||||
memory: MemoryConfig::default(),
|
||||
storage: StorageConfig::default(),
|
||||
@@ -6205,6 +6352,7 @@ default_temperature = 0.7
|
||||
cost: CostConfig::default(),
|
||||
peripherals: PeripheralsConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
swarms: HashMap::new(),
|
||||
hooks: HooksConfig::default(),
|
||||
hardware: HardwareConfig::default(),
|
||||
transcription: TranscriptionConfig::default(),
|
||||
@@ -6496,6 +6644,7 @@ tool_dispatcher = "xml"
|
||||
cost: CostConfig::default(),
|
||||
peripherals: PeripheralsConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
swarms: HashMap::new(),
|
||||
hooks: HooksConfig::default(),
|
||||
hardware: HardwareConfig::default(),
|
||||
transcription: TranscriptionConfig::default(),
|
||||
@@ -6538,6 +6687,15 @@ tool_dispatcher = "xml"
|
||||
config.browser.computer_use.api_key = Some("browser-credential".into());
|
||||
config.web_search.brave_api_key = Some("brave-credential".into());
|
||||
config.storage.provider.config.db_url = Some("postgres://user:pw@host/db".into());
|
||||
config.channels_config.feishu = Some(FeishuConfig {
|
||||
app_id: "cli_feishu_123".into(),
|
||||
app_secret: "feishu-secret".into(),
|
||||
encrypt_key: Some("feishu-encrypt".into()),
|
||||
verification_token: Some("feishu-verify".into()),
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
});
|
||||
|
||||
config.agents.insert(
|
||||
"worker".into(),
|
||||
@@ -6605,6 +6763,32 @@ tool_dispatcher = "xml"
|
||||
"postgres://user:pw@host/db"
|
||||
);
|
||||
|
||||
let feishu = stored.channels_config.feishu.as_ref().unwrap();
|
||||
assert!(crate::security::SecretStore::is_encrypted(
|
||||
&feishu.app_secret
|
||||
));
|
||||
assert_eq!(store.decrypt(&feishu.app_secret).unwrap(), "feishu-secret");
|
||||
assert!(feishu
|
||||
.encrypt_key
|
||||
.as_deref()
|
||||
.is_some_and(crate::security::SecretStore::is_encrypted));
|
||||
assert_eq!(
|
||||
store
|
||||
.decrypt(feishu.encrypt_key.as_deref().unwrap())
|
||||
.unwrap(),
|
||||
"feishu-encrypt"
|
||||
);
|
||||
assert!(feishu
|
||||
.verification_token
|
||||
.as_deref()
|
||||
.is_some_and(crate::security::SecretStore::is_encrypted));
|
||||
assert_eq!(
|
||||
store
|
||||
.decrypt(feishu.verification_token.as_deref().unwrap())
|
||||
.unwrap(),
|
||||
"feishu-verify"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
@@ -6865,6 +7049,8 @@ allowed_users = ["@ops:matrix.org"]
|
||||
clawdtalk: None,
|
||||
message_timeout_secs: 300,
|
||||
ack_reactions: true,
|
||||
show_tool_calls: true,
|
||||
session_persistence: true,
|
||||
};
|
||||
let toml_str = toml::to_string_pretty(&c).unwrap();
|
||||
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -6903,6 +7089,7 @@ allowed_users = ["@ops:matrix.org"]
|
||||
let json = r#"{"bot_token":"xoxb-tok"}"#;
|
||||
let parsed: SlackConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(parsed.allowed_users.is_empty());
|
||||
assert!(!parsed.interrupt_on_new_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6910,6 +7097,14 @@ allowed_users = ["@ops:matrix.org"]
|
||||
let json = r#"{"bot_token":"xoxb-tok","allowed_users":["U111"]}"#;
|
||||
let parsed: SlackConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(parsed.allowed_users, vec!["U111"]);
|
||||
assert!(!parsed.interrupt_on_new_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn slack_config_deserializes_interrupt_on_new_message() {
|
||||
let json = r#"{"bot_token":"xoxb-tok","interrupt_on_new_message":true}"#;
|
||||
let parsed: SlackConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(parsed.interrupt_on_new_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6931,6 +7126,7 @@ channel_id = "C123"
|
||||
"#;
|
||||
let parsed: SlackConfig = toml::from_str(toml_str).unwrap();
|
||||
assert!(parsed.allowed_users.is_empty());
|
||||
assert!(!parsed.interrupt_on_new_message);
|
||||
assert_eq!(parsed.channel_id.as_deref(), Some("C123"));
|
||||
}
|
||||
|
||||
@@ -7081,6 +7277,8 @@ channel_id = "C123"
|
||||
clawdtalk: None,
|
||||
message_timeout_secs: 300,
|
||||
ack_reactions: true,
|
||||
show_tool_calls: true,
|
||||
session_persistence: true,
|
||||
};
|
||||
let toml_str = toml::to_string_pretty(&c).unwrap();
|
||||
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
|
||||
@@ -7704,6 +7902,40 @@ requires_openai_auth = true
|
||||
assert_eq!(config.api_key.as_deref(), Some("sk-test-codex-key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn save_repairs_bare_config_filename_using_runtime_resolution() {
|
||||
let _env_guard = env_override_lock().await;
|
||||
let temp_home =
|
||||
std::env::temp_dir().join(format!("zeroclaw_test_home_{}", uuid::Uuid::new_v4()));
|
||||
let workspace_dir = temp_home.join("workspace");
|
||||
let resolved_config_path = temp_home.join(".zeroclaw").join("config.toml");
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
std::env::set_var("HOME", &temp_home);
|
||||
std::env::set_var("ZEROCLAW_WORKSPACE", &workspace_dir);
|
||||
|
||||
let mut config = Config::default();
|
||||
config.workspace_dir = workspace_dir;
|
||||
config.config_path = PathBuf::from("config.toml");
|
||||
config.default_temperature = 0.5;
|
||||
config.save().await.unwrap();
|
||||
|
||||
assert!(resolved_config_path.exists());
|
||||
let saved = tokio::fs::read_to_string(&resolved_config_path)
|
||||
.await
|
||||
.unwrap();
|
||||
let parsed: Config = toml::from_str(&saved).unwrap();
|
||||
assert_eq!(parsed.default_temperature, 0.5);
|
||||
|
||||
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||
if let Some(home) = original_home {
|
||||
std::env::set_var("HOME", home);
|
||||
} else {
|
||||
std::env::remove_var("HOME");
|
||||
}
|
||||
let _ = tokio::fs::remove_dir_all(temp_home).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn validate_ollama_cloud_model_requires_remote_api_url() {
|
||||
let _env_guard = env_override_lock().await;
|
||||
@@ -7990,6 +8222,49 @@ default_model = "legacy-model"
|
||||
let _ = fs::remove_dir_all(temp_home).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn load_or_init_decrypts_feishu_channel_secrets() {
|
||||
let _env_guard = env_override_lock().await;
|
||||
let temp_home =
|
||||
std::env::temp_dir().join(format!("zeroclaw_test_home_{}", uuid::Uuid::new_v4()));
|
||||
let config_dir = temp_home.join(".zeroclaw");
|
||||
let config_path = config_dir.join("config.toml");
|
||||
|
||||
fs::create_dir_all(&config_dir).await.unwrap();
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
std::env::set_var("HOME", &temp_home);
|
||||
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||
|
||||
let mut config = Config::default();
|
||||
config.config_path = config_path.clone();
|
||||
config.workspace_dir = config_dir.join("workspace");
|
||||
config.secrets.encrypt = true;
|
||||
config.channels_config.feishu = Some(FeishuConfig {
|
||||
app_id: "cli_feishu_123".into(),
|
||||
app_secret: "feishu-secret".into(),
|
||||
encrypt_key: Some("feishu-encrypt".into()),
|
||||
verification_token: Some("feishu-verify".into()),
|
||||
allowed_users: vec!["*".into()],
|
||||
receive_mode: LarkReceiveMode::Websocket,
|
||||
port: None,
|
||||
});
|
||||
config.save().await.unwrap();
|
||||
|
||||
let loaded = Config::load_or_init().await.unwrap();
|
||||
let feishu = loaded.channels_config.feishu.as_ref().unwrap();
|
||||
assert_eq!(feishu.app_secret, "feishu-secret");
|
||||
assert_eq!(feishu.encrypt_key.as_deref(), Some("feishu-encrypt"));
|
||||
assert_eq!(feishu.verification_token.as_deref(), Some("feishu-verify"));
|
||||
|
||||
if let Some(home) = original_home {
|
||||
std::env::set_var("HOME", home);
|
||||
} else {
|
||||
std::env::remove_var("HOME");
|
||||
}
|
||||
let _ = fs::remove_dir_all(temp_home).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn load_or_init_uses_persisted_active_workspace_marker() {
|
||||
let _env_guard = env_override_lock().await;
|
||||
@@ -9122,4 +9397,72 @@ require_otp_to_resume = true
|
||||
assert_eq!(&deserialized, variant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn swarm_strategy_roundtrip() {
|
||||
let cases = vec![
|
||||
(SwarmStrategy::Sequential, "\"sequential\""),
|
||||
(SwarmStrategy::Parallel, "\"parallel\""),
|
||||
(SwarmStrategy::Router, "\"router\""),
|
||||
];
|
||||
for (variant, expected_json) in &cases {
|
||||
let serialized = serde_json::to_string(variant).expect("serialize");
|
||||
assert_eq!(&serialized, expected_json, "variant: {variant:?}");
|
||||
let deserialized: SwarmStrategy =
|
||||
serde_json::from_str(expected_json).expect("deserialize");
|
||||
assert_eq!(&deserialized, variant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn swarm_config_deserializes_with_defaults() {
|
||||
let toml_str = r#"
|
||||
agents = ["researcher", "writer"]
|
||||
strategy = "sequential"
|
||||
"#;
|
||||
let config: SwarmConfig = toml::from_str(toml_str).expect("deserialize");
|
||||
assert_eq!(config.agents, vec!["researcher", "writer"]);
|
||||
assert_eq!(config.strategy, SwarmStrategy::Sequential);
|
||||
assert!(config.router_prompt.is_none());
|
||||
assert!(config.description.is_none());
|
||||
assert_eq!(config.timeout_secs, 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn swarm_config_deserializes_full() {
|
||||
let toml_str = r#"
|
||||
agents = ["a", "b", "c"]
|
||||
strategy = "router"
|
||||
router_prompt = "Pick the best."
|
||||
description = "Multi-agent router"
|
||||
timeout_secs = 120
|
||||
"#;
|
||||
let config: SwarmConfig = toml::from_str(toml_str).expect("deserialize");
|
||||
assert_eq!(config.agents.len(), 3);
|
||||
assert_eq!(config.strategy, SwarmStrategy::Router);
|
||||
assert_eq!(config.router_prompt.as_deref(), Some("Pick the best."));
|
||||
assert_eq!(config.description.as_deref(), Some("Multi-agent router"));
|
||||
assert_eq!(config.timeout_secs, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn config_with_swarms_section_deserializes() {
|
||||
let toml_str = r#"
|
||||
[agents.researcher]
|
||||
provider = "ollama"
|
||||
model = "llama3"
|
||||
|
||||
[agents.writer]
|
||||
provider = "openrouter"
|
||||
model = "claude-sonnet"
|
||||
|
||||
[swarms.pipeline]
|
||||
agents = ["researcher", "writer"]
|
||||
strategy = "sequential"
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml_str).expect("deserialize");
|
||||
assert_eq!(config.agents.len(), 2);
|
||||
assert_eq!(config.swarms.len(), 1);
|
||||
assert!(config.swarms.contains_key("pipeline"));
|
||||
}
|
||||
}
|
||||
|
||||
+172
-21
@@ -152,44 +152,122 @@ pub fn handle_command(command: crate::CronCommands, config: &Config) -> Result<(
|
||||
crate::CronCommands::Add {
|
||||
expression,
|
||||
tz,
|
||||
agent,
|
||||
command,
|
||||
} => {
|
||||
let schedule = Schedule::Cron {
|
||||
expr: expression,
|
||||
tz,
|
||||
};
|
||||
let job = add_shell_job(config, None, schedule, &command)?;
|
||||
println!("✅ Added cron job {}", job.id);
|
||||
println!(" Expr: {}", job.expression);
|
||||
println!(" Next: {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
if agent {
|
||||
let job = add_agent_job(
|
||||
config,
|
||||
None,
|
||||
schedule,
|
||||
&command,
|
||||
SessionTarget::Isolated,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)?;
|
||||
println!("✅ Added agent cron job {}", job.id);
|
||||
println!(" Expr : {}", job.expression);
|
||||
println!(" Next : {}", job.next_run.to_rfc3339());
|
||||
println!(" Prompt: {}", job.prompt.as_deref().unwrap_or_default());
|
||||
} else {
|
||||
let job = add_shell_job(config, None, schedule, &command)?;
|
||||
println!("✅ Added cron job {}", job.id);
|
||||
println!(" Expr: {}", job.expression);
|
||||
println!(" Next: {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
crate::CronCommands::AddAt { at, command } => {
|
||||
crate::CronCommands::AddAt { at, agent, command } => {
|
||||
let at = chrono::DateTime::parse_from_rfc3339(&at)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid RFC3339 timestamp for --at: {e}"))?
|
||||
.with_timezone(&chrono::Utc);
|
||||
let schedule = Schedule::At { at };
|
||||
let job = add_shell_job(config, None, schedule, &command)?;
|
||||
println!("✅ Added one-shot cron job {}", job.id);
|
||||
println!(" At : {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
if agent {
|
||||
let job = add_agent_job(
|
||||
config,
|
||||
None,
|
||||
schedule,
|
||||
&command,
|
||||
SessionTarget::Isolated,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
)?;
|
||||
println!("✅ Added one-shot agent cron job {}", job.id);
|
||||
println!(" At : {}", job.next_run.to_rfc3339());
|
||||
println!(" Prompt: {}", job.prompt.as_deref().unwrap_or_default());
|
||||
} else {
|
||||
let job = add_shell_job(config, None, schedule, &command)?;
|
||||
println!("✅ Added one-shot cron job {}", job.id);
|
||||
println!(" At : {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
crate::CronCommands::AddEvery { every_ms, command } => {
|
||||
crate::CronCommands::AddEvery {
|
||||
every_ms,
|
||||
agent,
|
||||
command,
|
||||
} => {
|
||||
let schedule = Schedule::Every { every_ms };
|
||||
let job = add_shell_job(config, None, schedule, &command)?;
|
||||
println!("✅ Added interval cron job {}", job.id);
|
||||
println!(" Every(ms): {every_ms}");
|
||||
println!(" Next : {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
if agent {
|
||||
let job = add_agent_job(
|
||||
config,
|
||||
None,
|
||||
schedule,
|
||||
&command,
|
||||
SessionTarget::Isolated,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)?;
|
||||
println!("✅ Added interval agent cron job {}", job.id);
|
||||
println!(" Every(ms): {every_ms}");
|
||||
println!(" Next : {}", job.next_run.to_rfc3339());
|
||||
println!(" Prompt : {}", job.prompt.as_deref().unwrap_or_default());
|
||||
} else {
|
||||
let job = add_shell_job(config, None, schedule, &command)?;
|
||||
println!("✅ Added interval cron job {}", job.id);
|
||||
println!(" Every(ms): {every_ms}");
|
||||
println!(" Next : {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
crate::CronCommands::Once { delay, command } => {
|
||||
let job = add_once(config, &delay, &command)?;
|
||||
println!("✅ Added one-shot cron job {}", job.id);
|
||||
println!(" At : {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
crate::CronCommands::Once {
|
||||
delay,
|
||||
agent,
|
||||
command,
|
||||
} => {
|
||||
if agent {
|
||||
let duration = parse_delay(&delay)?;
|
||||
let at = chrono::Utc::now() + duration;
|
||||
let schedule = Schedule::At { at };
|
||||
let job = add_agent_job(
|
||||
config,
|
||||
None,
|
||||
schedule,
|
||||
&command,
|
||||
SessionTarget::Isolated,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
)?;
|
||||
println!("✅ Added one-shot agent cron job {}", job.id);
|
||||
println!(" At : {}", job.next_run.to_rfc3339());
|
||||
println!(" Prompt: {}", job.prompt.as_deref().unwrap_or_default());
|
||||
} else {
|
||||
let job = add_once(config, &delay, &command)?;
|
||||
println!("✅ Added one-shot cron job {}", job.id);
|
||||
println!(" At : {}", job.next_run.to_rfc3339());
|
||||
println!(" Cmd : {}", job.command);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
crate::CronCommands::Update {
|
||||
@@ -686,4 +764,77 @@ mod tests {
|
||||
.to_string()
|
||||
.contains("blocked by security policy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_agent_flag_creates_agent_job() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = test_config(&tmp);
|
||||
|
||||
handle_command(
|
||||
crate::CronCommands::Add {
|
||||
expression: "*/15 * * * *".into(),
|
||||
tz: None,
|
||||
agent: true,
|
||||
command: "Check server health: disk space, memory, CPU load".into(),
|
||||
},
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let jobs = list_jobs(&config).unwrap();
|
||||
assert_eq!(jobs.len(), 1);
|
||||
assert_eq!(jobs[0].job_type, JobType::Agent);
|
||||
assert_eq!(
|
||||
jobs[0].prompt.as_deref(),
|
||||
Some("Check server health: disk space, memory, CPU load")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_agent_flag_bypasses_shell_security_validation() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut config = test_config(&tmp);
|
||||
config.autonomy.allowed_commands = vec!["echo".into()];
|
||||
config.autonomy.level = crate::security::AutonomyLevel::Supervised;
|
||||
|
||||
// Without --agent, a natural language string would be blocked by shell
|
||||
// security policy. With --agent, it routes to agent job and skips
|
||||
// shell validation entirely.
|
||||
let result = handle_command(
|
||||
crate::CronCommands::Add {
|
||||
expression: "*/15 * * * *".into(),
|
||||
tz: None,
|
||||
agent: true,
|
||||
command: "Check server health: disk space, memory, CPU load".into(),
|
||||
},
|
||||
&config,
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let jobs = list_jobs(&config).unwrap();
|
||||
assert_eq!(jobs.len(), 1);
|
||||
assert_eq!(jobs[0].job_type, JobType::Agent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_without_agent_flag_defaults_to_shell_job() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = test_config(&tmp);
|
||||
|
||||
handle_command(
|
||||
crate::CronCommands::Add {
|
||||
expression: "*/5 * * * *".into(),
|
||||
tz: None,
|
||||
agent: false,
|
||||
command: "echo ok".into(),
|
||||
},
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let jobs = list_jobs(&config).unwrap();
|
||||
assert_eq!(jobs.len(), 1);
|
||||
assert_eq!(jobs[0].job_type, JobType::Shell);
|
||||
assert_eq!(jobs[0].command, "echo ok");
|
||||
}
|
||||
}
|
||||
|
||||
+18
-1
@@ -1,7 +1,8 @@
|
||||
#[cfg(feature = "channel-matrix")]
|
||||
use crate::channels::MatrixChannel;
|
||||
use crate::channels::{
|
||||
Channel, DiscordChannel, MattermostChannel, SendMessage, SlackChannel, TelegramChannel,
|
||||
Channel, DiscordChannel, MattermostChannel, SendMessage, SignalChannel, SlackChannel,
|
||||
TelegramChannel,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::cron::{
|
||||
@@ -378,6 +379,22 @@ pub(crate) async fn deliver_announcement(
|
||||
);
|
||||
channel.send(&SendMessage::new(output, target)).await?;
|
||||
}
|
||||
"signal" => {
|
||||
let sg = config
|
||||
.channels_config
|
||||
.signal
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("signal channel not configured"))?;
|
||||
let channel = SignalChannel::new(
|
||||
sg.http_url.clone(),
|
||||
sg.account.clone(),
|
||||
sg.group_id.clone(),
|
||||
sg.allowed_from.clone(),
|
||||
sg.ignore_attachments,
|
||||
sg.ignore_stories,
|
||||
);
|
||||
channel.send(&SendMessage::new(output, target)).await?;
|
||||
}
|
||||
"matrix" => {
|
||||
#[cfg(feature = "channel-matrix")]
|
||||
{
|
||||
|
||||
+140
-58
@@ -203,14 +203,17 @@ where
|
||||
}
|
||||
|
||||
async fn run_heartbeat_worker(config: Config) -> Result<()> {
|
||||
use crate::heartbeat::engine::HeartbeatEngine;
|
||||
|
||||
let observer: std::sync::Arc<dyn crate::observability::Observer> =
|
||||
std::sync::Arc::from(crate::observability::create_observer(&config.observability));
|
||||
let engine = crate::heartbeat::engine::HeartbeatEngine::new(
|
||||
let engine = HeartbeatEngine::new(
|
||||
config.heartbeat.clone(),
|
||||
config.workspace_dir.clone(),
|
||||
observer,
|
||||
);
|
||||
let delivery = heartbeat_delivery_target(&config)?;
|
||||
let delivery = resolve_heartbeat_delivery(&config)?;
|
||||
let two_phase = config.heartbeat.two_phase;
|
||||
|
||||
let interval_mins = config.heartbeat.interval_minutes.max(5);
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(u64::from(interval_mins) * 60));
|
||||
@@ -218,14 +221,71 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let file_tasks = engine.collect_tasks().await?;
|
||||
let tasks = heartbeat_tasks_for_tick(file_tasks, config.heartbeat.message.as_deref());
|
||||
// Collect runnable tasks (active only, sorted by priority)
|
||||
let mut tasks = engine.collect_runnable_tasks().await?;
|
||||
if tasks.is_empty() {
|
||||
continue;
|
||||
// Try fallback message
|
||||
if let Some(fallback) = config
|
||||
.heartbeat
|
||||
.message
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|m| !m.is_empty())
|
||||
{
|
||||
tasks.push(crate::heartbeat::engine::HeartbeatTask {
|
||||
text: fallback.to_string(),
|
||||
priority: crate::heartbeat::engine::TaskPriority::Medium,
|
||||
status: crate::heartbeat::engine::TaskStatus::Active,
|
||||
});
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let prompt = format!("[Heartbeat Task] {task}");
|
||||
// ── Phase 1: LLM decision (two-phase mode) ──────────────
|
||||
let tasks_to_run = if two_phase {
|
||||
let decision_prompt = HeartbeatEngine::build_decision_prompt(&tasks);
|
||||
match crate::agent::run(
|
||||
config.clone(),
|
||||
Some(decision_prompt),
|
||||
None,
|
||||
None,
|
||||
0.0, // Low temperature for deterministic decision
|
||||
vec![],
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let indices = HeartbeatEngine::parse_decision_response(&response, tasks.len());
|
||||
if indices.is_empty() {
|
||||
tracing::info!("💓 Heartbeat Phase 1: skip (nothing to do)");
|
||||
crate::health::mark_component_ok("heartbeat");
|
||||
continue;
|
||||
}
|
||||
tracing::info!(
|
||||
"💓 Heartbeat Phase 1: run {} of {} tasks",
|
||||
indices.len(),
|
||||
tasks.len()
|
||||
);
|
||||
indices
|
||||
.into_iter()
|
||||
.filter_map(|i| tasks.get(i).cloned())
|
||||
.collect()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("💓 Heartbeat Phase 1 failed, running all tasks: {e}");
|
||||
tasks
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tasks
|
||||
};
|
||||
|
||||
// ── Phase 2: Execute selected tasks ─────────────────────
|
||||
for task in &tasks_to_run {
|
||||
let prompt = format!("[Heartbeat Task | {}] {}", task.priority, task.text);
|
||||
let temp = config.default_temperature;
|
||||
match crate::agent::run(
|
||||
config.clone(),
|
||||
@@ -242,7 +302,7 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> {
|
||||
Ok(output) => {
|
||||
crate::health::mark_component_ok("heartbeat");
|
||||
let announcement = if output.trim().is_empty() {
|
||||
"heartbeat task executed".to_string()
|
||||
format!("💓 heartbeat task completed: {}", task.text)
|
||||
} else {
|
||||
output
|
||||
};
|
||||
@@ -272,22 +332,8 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn heartbeat_tasks_for_tick(
|
||||
file_tasks: Vec<String>,
|
||||
fallback_message: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
if !file_tasks.is_empty() {
|
||||
return file_tasks;
|
||||
}
|
||||
|
||||
fallback_message
|
||||
.map(str::trim)
|
||||
.filter(|message| !message.is_empty())
|
||||
.map(|message| vec![message.to_string()])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn heartbeat_delivery_target(config: &Config) -> Result<Option<(String, String)>> {
|
||||
/// Resolve delivery target: explicit config > auto-detect first configured channel.
|
||||
fn resolve_heartbeat_delivery(config: &Config) -> Result<Option<(String, String)>> {
|
||||
let channel = config
|
||||
.heartbeat
|
||||
.target
|
||||
@@ -302,16 +348,45 @@ fn heartbeat_delivery_target(config: &Config) -> Result<Option<(String, String)>
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
match (channel, target) {
|
||||
(None, None) => Ok(None),
|
||||
(Some(_), None) => anyhow::bail!("heartbeat.to is required when heartbeat.target is set"),
|
||||
(None, Some(_)) => anyhow::bail!("heartbeat.target is required when heartbeat.to is set"),
|
||||
// Both explicitly set — validate and use.
|
||||
(Some(channel), Some(target)) => {
|
||||
validate_heartbeat_channel_config(config, channel)?;
|
||||
Ok(Some((channel.to_string(), target.to_string())))
|
||||
}
|
||||
// Only one set — error.
|
||||
(Some(_), None) => anyhow::bail!("heartbeat.to is required when heartbeat.target is set"),
|
||||
(None, Some(_)) => anyhow::bail!("heartbeat.target is required when heartbeat.to is set"),
|
||||
// Neither set — try auto-detect the first configured channel.
|
||||
(None, None) => Ok(auto_detect_heartbeat_channel(config)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-detect the best channel for heartbeat delivery by checking which
|
||||
/// channels are configured. Returns the first match in priority order.
|
||||
fn auto_detect_heartbeat_channel(config: &Config) -> Option<(String, String)> {
|
||||
// Priority order: telegram > discord > slack > mattermost
|
||||
if let Some(tg) = &config.channels_config.telegram {
|
||||
// Use the first allowed_user as target, or fall back to empty (broadcast)
|
||||
let target = tg.allowed_users.first().cloned().unwrap_or_default();
|
||||
if !target.is_empty() {
|
||||
return Some(("telegram".to_string(), target));
|
||||
}
|
||||
}
|
||||
if config.channels_config.discord.is_some() {
|
||||
// Discord requires explicit target — can't auto-detect
|
||||
return None;
|
||||
}
|
||||
if config.channels_config.slack.is_some() {
|
||||
// Slack requires explicit target
|
||||
return None;
|
||||
}
|
||||
if config.channels_config.mattermost.is_some() {
|
||||
// Mattermost requires explicit target
|
||||
return None;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn validate_heartbeat_channel_config(config: &Config, channel: &str) -> Result<()> {
|
||||
match channel.to_ascii_lowercase().as_str() {
|
||||
"telegram" => {
|
||||
@@ -487,75 +562,56 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_tasks_use_file_tasks_when_available() {
|
||||
let tasks =
|
||||
heartbeat_tasks_for_tick(vec!["From file".to_string()], Some("Fallback from config"));
|
||||
assert_eq!(tasks, vec!["From file".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_tasks_fall_back_to_config_message() {
|
||||
let tasks = heartbeat_tasks_for_tick(vec![], Some(" check london time "));
|
||||
assert_eq!(tasks, vec!["check london time".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_tasks_ignore_empty_fallback_message() {
|
||||
let tasks = heartbeat_tasks_for_tick(vec![], Some(" "));
|
||||
assert!(tasks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_delivery_target_none_when_unset() {
|
||||
fn resolve_delivery_none_when_unset() {
|
||||
let config = Config::default();
|
||||
let target = heartbeat_delivery_target(&config).unwrap();
|
||||
let target = resolve_heartbeat_delivery(&config).unwrap();
|
||||
assert!(target.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_delivery_target_requires_to_field() {
|
||||
fn resolve_delivery_requires_to_field() {
|
||||
let mut config = Config::default();
|
||||
config.heartbeat.target = Some("telegram".into());
|
||||
let err = heartbeat_delivery_target(&config).unwrap_err();
|
||||
let err = resolve_heartbeat_delivery(&config).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("heartbeat.to is required when heartbeat.target is set"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_delivery_target_requires_target_field() {
|
||||
fn resolve_delivery_requires_target_field() {
|
||||
let mut config = Config::default();
|
||||
config.heartbeat.to = Some("123456".into());
|
||||
let err = heartbeat_delivery_target(&config).unwrap_err();
|
||||
let err = resolve_heartbeat_delivery(&config).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("heartbeat.target is required when heartbeat.to is set"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_delivery_target_rejects_unsupported_channel() {
|
||||
fn resolve_delivery_rejects_unsupported_channel() {
|
||||
let mut config = Config::default();
|
||||
config.heartbeat.target = Some("email".into());
|
||||
config.heartbeat.to = Some("ops@example.com".into());
|
||||
let err = heartbeat_delivery_target(&config).unwrap_err();
|
||||
let err = resolve_heartbeat_delivery(&config).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("unsupported heartbeat.target channel"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_delivery_target_requires_channel_configuration() {
|
||||
fn resolve_delivery_requires_channel_configuration() {
|
||||
let mut config = Config::default();
|
||||
config.heartbeat.target = Some("telegram".into());
|
||||
config.heartbeat.to = Some("123456".into());
|
||||
let err = heartbeat_delivery_target(&config).unwrap_err();
|
||||
let err = resolve_heartbeat_delivery(&config).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("channels_config.telegram is not configured"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_delivery_target_accepts_telegram_configuration() {
|
||||
fn resolve_delivery_accepts_telegram_configuration() {
|
||||
let mut config = Config::default();
|
||||
config.heartbeat.target = Some("telegram".into());
|
||||
config.heartbeat.to = Some("123456".into());
|
||||
@@ -568,7 +624,33 @@ mod tests {
|
||||
mention_only: false,
|
||||
});
|
||||
|
||||
let target = heartbeat_delivery_target(&config).unwrap();
|
||||
let target = resolve_heartbeat_delivery(&config).unwrap();
|
||||
assert_eq!(target, Some(("telegram".to_string(), "123456".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_detect_telegram_when_configured() {
|
||||
let mut config = Config::default();
|
||||
config.channels_config.telegram = Some(crate::config::TelegramConfig {
|
||||
bot_token: "bot-token".into(),
|
||||
allowed_users: vec!["user123".into()],
|
||||
stream_mode: crate::config::StreamMode::default(),
|
||||
draft_update_interval_ms: 1000,
|
||||
interrupt_on_new_message: false,
|
||||
mention_only: false,
|
||||
});
|
||||
|
||||
let target = resolve_heartbeat_delivery(&config).unwrap();
|
||||
assert_eq!(
|
||||
target,
|
||||
Some(("telegram".to_string(), "user123".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_detect_none_when_no_channels() {
|
||||
let config = Config::default();
|
||||
let target = auto_detect_heartbeat_channel(&config);
|
||||
assert!(target.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -364,8 +364,9 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
|
||||
.clone()
|
||||
.unwrap_or_else(|| "anthropic/claude-sonnet-4".into());
|
||||
let temperature = config.default_temperature;
|
||||
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory_with_storage(
|
||||
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory_with_storage_and_routes(
|
||||
&config.memory,
|
||||
&config.embedding_routes,
|
||||
Some(&config.storage.provider.config),
|
||||
&config.workspace_dir,
|
||||
config.api_key.as_deref(),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use axum::{
|
||||
http::{header, StatusCode, Uri},
|
||||
response::IntoResponse,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use rust_embed::Embed;
|
||||
|
||||
@@ -13,18 +13,29 @@ use rust_embed::Embed;
|
||||
struct WebAssets;
|
||||
|
||||
/// Serve static files from `/_app/*` path
|
||||
pub async fn handle_static(uri: Uri) -> impl IntoResponse {
|
||||
let path = uri.path().strip_prefix("/_app/").unwrap_or(uri.path());
|
||||
pub async fn handle_static(uri: Uri) -> Response {
|
||||
let path = uri
|
||||
.path()
|
||||
.strip_prefix("/_app/")
|
||||
.unwrap_or(uri.path())
|
||||
.trim_start_matches('/');
|
||||
|
||||
serve_embedded_file(path)
|
||||
}
|
||||
|
||||
/// SPA fallback: serve index.html for any non-API, non-static GET request
|
||||
pub async fn handle_spa_fallback() -> impl IntoResponse {
|
||||
pub async fn handle_spa_fallback() -> Response {
|
||||
if WebAssets::get("index.html").is_none() {
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"Web dashboard not available. Build it with: cd web && npm ci && npm run build",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
serve_embedded_file("index.html")
|
||||
}
|
||||
|
||||
fn serve_embedded_file(path: &str) -> impl IntoResponse {
|
||||
fn serve_embedded_file(path: &str) -> Response {
|
||||
match WebAssets::get(path) {
|
||||
Some(content) => {
|
||||
let mime = mime_guess::from_path(path)
|
||||
|
||||
+13
-39
@@ -119,6 +119,17 @@ pub async fn handle_ws_chat(
|
||||
async fn handle_socket(socket: WebSocket, state: AppState, _session_id: Option<String>) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
// Build a persistent Agent for this connection so history is maintained across turns.
|
||||
let config = state.config.lock().clone();
|
||||
let mut agent = match crate::agent::Agent::from_config(&config) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let err = serde_json::json!({"type": "error", "message": format!("Failed to initialise agent: {e}")});
|
||||
let _ = sender.send(Message::Text(err.to_string().into())).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(msg) = receiver.next().await {
|
||||
let msg = match msg {
|
||||
Ok(Message::Text(text)) => text,
|
||||
@@ -161,45 +172,8 @@ async fn handle_socket(socket: WebSocket, state: AppState, _session_id: Option<S
|
||||
"model": state.model,
|
||||
}));
|
||||
|
||||
// Simple single-turn chat (no streaming for now — use provider.chat_with_system)
|
||||
let system_prompt = {
|
||||
let config_guard = state.config.lock();
|
||||
crate::channels::build_system_prompt(
|
||||
&config_guard.workspace_dir,
|
||||
&state.model,
|
||||
&[],
|
||||
&[],
|
||||
Some(&config_guard.identity),
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
let messages = vec![
|
||||
crate::providers::ChatMessage::system(system_prompt),
|
||||
crate::providers::ChatMessage::user(&content),
|
||||
];
|
||||
|
||||
let multimodal_config = state.config.lock().multimodal.clone();
|
||||
let prepared =
|
||||
match crate::multimodal::prepare_messages_for_provider(&messages, &multimodal_config)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
let err = serde_json::json!({
|
||||
"type": "error",
|
||||
"message": format!("Multimodal prep failed: {e}")
|
||||
});
|
||||
let _ = sender.send(Message::Text(err.to_string().into())).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match state
|
||||
.provider
|
||||
.chat_with_history(&prepared.messages, &state.model, state.temperature)
|
||||
.await
|
||||
{
|
||||
// Multi-turn chat via persistent Agent (history is maintained across turns)
|
||||
match agent.turn(&content).await {
|
||||
Ok(response) => {
|
||||
// Send the full response as a done message
|
||||
let done = serde_json::json!({
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
pub mod types;
|
||||
|
||||
pub use types::{Hand, HandContext, HandRun, HandRunStatus};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
|
||||
/// Load all hand definitions from TOML files in the given directory.
|
||||
///
|
||||
/// Each `.toml` file in `hands_dir` is expected to deserialize into a [`Hand`].
|
||||
/// Files that fail to parse are logged and skipped.
|
||||
pub fn load_hands(hands_dir: &Path) -> Result<Vec<Hand>> {
|
||||
if !hands_dir.is_dir() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut hands = Vec::new();
|
||||
let entries = std::fs::read_dir(hands_dir)
|
||||
.with_context(|| format!("failed to read hands directory: {}", hands_dir.display()))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
|
||||
continue;
|
||||
}
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read hand file: {}", path.display()))?;
|
||||
match toml::from_str::<Hand>(&content) {
|
||||
Ok(hand) => hands.push(hand),
|
||||
Err(e) => {
|
||||
tracing::warn!(path = %path.display(), error = %e, "skipping malformed hand file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hands)
|
||||
}
|
||||
|
||||
/// Load the rolling context for a hand.
|
||||
///
|
||||
/// Reads from `{hands_dir}/{name}/context.json`. Returns a fresh
|
||||
/// [`HandContext`] if the file does not exist yet.
|
||||
pub fn load_hand_context(hands_dir: &Path, name: &str) -> Result<HandContext> {
|
||||
let path = hands_dir.join(name).join("context.json");
|
||||
if !path.exists() {
|
||||
return Ok(HandContext::new(name));
|
||||
}
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read hand context: {}", path.display()))?;
|
||||
let ctx: HandContext = serde_json::from_str(&content)
|
||||
.with_context(|| format!("failed to parse hand context: {}", path.display()))?;
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
/// Persist the rolling context for a hand.
|
||||
///
|
||||
/// Writes to `{hands_dir}/{name}/context.json`, creating the
|
||||
/// directory if it does not exist.
|
||||
pub fn save_hand_context(hands_dir: &Path, context: &HandContext) -> Result<()> {
|
||||
let dir = hands_dir.join(&context.hand_name);
|
||||
std::fs::create_dir_all(&dir)
|
||||
.with_context(|| format!("failed to create hand context dir: {}", dir.display()))?;
|
||||
let path = dir.join("context.json");
|
||||
let json = serde_json::to_string_pretty(context)?;
|
||||
std::fs::write(&path, json)
|
||||
.with_context(|| format!("failed to write hand context: {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn write_hand_toml(dir: &Path, filename: &str, content: &str) {
|
||||
std::fs::write(dir.join(filename), content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_hands_empty_dir() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let hands = load_hands(tmp.path()).unwrap();
|
||||
assert!(hands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_hands_nonexistent_dir() {
|
||||
let hands = load_hands(Path::new("/nonexistent/path/hands")).unwrap();
|
||||
assert!(hands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_hands_parses_valid_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
write_hand_toml(
|
||||
tmp.path(),
|
||||
"scanner.toml",
|
||||
r#"
|
||||
name = "scanner"
|
||||
description = "Market scanner"
|
||||
prompt = "Scan markets."
|
||||
|
||||
[schedule]
|
||||
kind = "cron"
|
||||
expr = "0 9 * * *"
|
||||
"#,
|
||||
);
|
||||
write_hand_toml(
|
||||
tmp.path(),
|
||||
"digest.toml",
|
||||
r#"
|
||||
name = "digest"
|
||||
description = "News digest"
|
||||
prompt = "Digest news."
|
||||
|
||||
[schedule]
|
||||
kind = "every"
|
||||
every_ms = 3600000
|
||||
"#,
|
||||
);
|
||||
|
||||
let hands = load_hands(tmp.path()).unwrap();
|
||||
assert_eq!(hands.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_hands_skips_malformed_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
write_hand_toml(tmp.path(), "bad.toml", "this is not valid toml struct");
|
||||
write_hand_toml(
|
||||
tmp.path(),
|
||||
"good.toml",
|
||||
r#"
|
||||
name = "good"
|
||||
description = "A good hand"
|
||||
prompt = "Do good things."
|
||||
|
||||
[schedule]
|
||||
kind = "every"
|
||||
every_ms = 60000
|
||||
"#,
|
||||
);
|
||||
|
||||
let hands = load_hands(tmp.path()).unwrap();
|
||||
assert_eq!(hands.len(), 1);
|
||||
assert_eq!(hands[0].name, "good");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_hands_ignores_non_toml_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("readme.md"), "# Hands").unwrap();
|
||||
std::fs::write(tmp.path().join("notes.txt"), "some notes").unwrap();
|
||||
|
||||
let hands = load_hands(tmp.path()).unwrap();
|
||||
assert!(hands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_roundtrip_through_filesystem() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut ctx = HandContext::new("test-hand");
|
||||
let run = HandRun {
|
||||
hand_name: "test-hand".into(),
|
||||
run_id: "run-001".into(),
|
||||
started_at: chrono::Utc::now(),
|
||||
finished_at: Some(chrono::Utc::now()),
|
||||
status: HandRunStatus::Completed,
|
||||
findings: vec!["found something".into()],
|
||||
knowledge_added: vec!["learned something".into()],
|
||||
duration_ms: Some(500),
|
||||
};
|
||||
ctx.record_run(run, 100);
|
||||
|
||||
save_hand_context(tmp.path(), &ctx).unwrap();
|
||||
let loaded = load_hand_context(tmp.path(), "test-hand").unwrap();
|
||||
|
||||
assert_eq!(loaded.hand_name, "test-hand");
|
||||
assert_eq!(loaded.total_runs, 1);
|
||||
assert_eq!(loaded.history.len(), 1);
|
||||
assert_eq!(loaded.learned_facts, vec!["learned something"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_context_returns_fresh_when_missing() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = load_hand_context(tmp.path(), "nonexistent").unwrap();
|
||||
assert_eq!(ctx.hand_name, "nonexistent");
|
||||
assert_eq!(ctx.total_runs, 0);
|
||||
assert!(ctx.history.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_context_creates_directory() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = HandContext::new("new-hand");
|
||||
save_hand_context(tmp.path(), &ctx).unwrap();
|
||||
|
||||
assert!(tmp.path().join("new-hand").join("context.json").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_then_load_preserves_multiple_runs() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut ctx = HandContext::new("multi");
|
||||
|
||||
for i in 0..5 {
|
||||
let run = HandRun {
|
||||
hand_name: "multi".into(),
|
||||
run_id: format!("run-{i:03}"),
|
||||
started_at: chrono::Utc::now(),
|
||||
finished_at: Some(chrono::Utc::now()),
|
||||
status: HandRunStatus::Completed,
|
||||
findings: vec![format!("finding-{i}")],
|
||||
knowledge_added: vec![format!("fact-{i}")],
|
||||
duration_ms: Some(100),
|
||||
};
|
||||
ctx.record_run(run, 3);
|
||||
}
|
||||
|
||||
save_hand_context(tmp.path(), &ctx).unwrap();
|
||||
let loaded = load_hand_context(tmp.path(), "multi").unwrap();
|
||||
|
||||
assert_eq!(loaded.total_runs, 5);
|
||||
assert_eq!(loaded.history.len(), 3, "history capped at max_history=3");
|
||||
assert_eq!(loaded.learned_facts.len(), 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::cron::Schedule;
|
||||
|
||||
// ── Hand ───────────────────────────────────────────────────────
|
||||
|
||||
/// A Hand is an autonomous agent package that runs on a schedule,
|
||||
/// accumulates knowledge over time, and reports results.
|
||||
///
|
||||
/// Hands are defined as TOML files in `~/.zeroclaw/hands/` and each
|
||||
/// maintains a rolling context of findings across runs so the agent
|
||||
/// grows smarter with every execution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Hand {
|
||||
/// Unique name (also used as directory/file stem)
|
||||
pub name: String,
|
||||
/// Human-readable description of what this hand does
|
||||
pub description: String,
|
||||
/// The schedule this hand runs on (reuses cron schedule types)
|
||||
pub schedule: Schedule,
|
||||
/// System prompt / execution plan for this hand
|
||||
pub prompt: String,
|
||||
/// Domain knowledge lines to inject into context
|
||||
#[serde(default)]
|
||||
pub knowledge: Vec<String>,
|
||||
/// Tools this hand is allowed to use (None = all available)
|
||||
#[serde(default)]
|
||||
pub allowed_tools: Option<Vec<String>>,
|
||||
/// Model override for this hand (None = default provider)
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
/// Whether this hand is currently active
|
||||
#[serde(default = "default_true")]
|
||||
pub active: bool,
|
||||
/// Maximum runs to keep in history
|
||||
#[serde(default = "default_max_runs")]
|
||||
pub max_history: usize,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_max_runs() -> usize {
|
||||
100
|
||||
}
|
||||
|
||||
// ── Hand Run ───────────────────────────────────────────────────
|
||||
|
||||
/// The status of a single hand execution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case", tag = "status")]
|
||||
pub enum HandRunStatus {
|
||||
Running,
|
||||
Completed,
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
/// Record of a single hand execution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HandRun {
|
||||
/// Name of the hand that produced this run
|
||||
pub hand_name: String,
|
||||
/// Unique identifier for this run
|
||||
pub run_id: String,
|
||||
/// When the run started
|
||||
pub started_at: DateTime<Utc>,
|
||||
/// When the run finished (None if still running)
|
||||
pub finished_at: Option<DateTime<Utc>>,
|
||||
/// Outcome of the run
|
||||
pub status: HandRunStatus,
|
||||
/// Key findings/outputs extracted from this run
|
||||
#[serde(default)]
|
||||
pub findings: Vec<String>,
|
||||
/// New knowledge accumulated and stored to memory
|
||||
#[serde(default)]
|
||||
pub knowledge_added: Vec<String>,
|
||||
/// Wall-clock duration in milliseconds
|
||||
pub duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
// ── Hand Context ───────────────────────────────────────────────
|
||||
|
||||
/// Rolling context that accumulates across hand runs.
|
||||
///
|
||||
/// Persisted as `~/.zeroclaw/hands/{name}/context.json`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HandContext {
|
||||
/// Name of the hand this context belongs to
|
||||
pub hand_name: String,
|
||||
/// Past runs, most-recent first, capped at `Hand::max_history`
|
||||
#[serde(default)]
|
||||
pub history: Vec<HandRun>,
|
||||
/// Persistent facts learned across runs
|
||||
#[serde(default)]
|
||||
pub learned_facts: Vec<String>,
|
||||
/// Timestamp of the last completed run
|
||||
pub last_run: Option<DateTime<Utc>>,
|
||||
/// Total number of successful runs
|
||||
#[serde(default)]
|
||||
pub total_runs: u64,
|
||||
}
|
||||
|
||||
impl HandContext {
|
||||
/// Create a fresh, empty context for a hand.
|
||||
pub fn new(hand_name: &str) -> Self {
|
||||
Self {
|
||||
hand_name: hand_name.to_string(),
|
||||
history: Vec::new(),
|
||||
learned_facts: Vec::new(),
|
||||
last_run: None,
|
||||
total_runs: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a completed run, updating counters and trimming history.
|
||||
pub fn record_run(&mut self, run: HandRun, max_history: usize) {
|
||||
if run.status == (HandRunStatus::Completed) {
|
||||
self.total_runs += 1;
|
||||
self.last_run = run.finished_at;
|
||||
}
|
||||
|
||||
// Merge new knowledge
|
||||
for fact in &run.knowledge_added {
|
||||
if !self.learned_facts.contains(fact) {
|
||||
self.learned_facts.push(fact.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the front (most-recent first)
|
||||
self.history.insert(0, run);
|
||||
|
||||
// Cap history length
|
||||
self.history.truncate(max_history);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cron::Schedule;
|
||||
|
||||
fn sample_hand() -> Hand {
|
||||
Hand {
|
||||
name: "market-scanner".into(),
|
||||
description: "Scans market trends and reports findings".into(),
|
||||
schedule: Schedule::Cron {
|
||||
expr: "0 9 * * 1-5".into(),
|
||||
tz: Some("America/New_York".into()),
|
||||
},
|
||||
prompt: "Scan market trends and report key findings.".into(),
|
||||
knowledge: vec!["Focus on tech sector.".into()],
|
||||
allowed_tools: Some(vec!["web_search".into(), "memory".into()]),
|
||||
model: Some("claude-opus-4-6".into()),
|
||||
active: true,
|
||||
max_history: 50,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_run(name: &str, status: HandRunStatus) -> HandRun {
|
||||
let now = Utc::now();
|
||||
HandRun {
|
||||
hand_name: name.into(),
|
||||
run_id: uuid::Uuid::new_v4().to_string(),
|
||||
started_at: now,
|
||||
finished_at: Some(now),
|
||||
status,
|
||||
findings: vec!["finding-1".into()],
|
||||
knowledge_added: vec!["learned-fact-A".into()],
|
||||
duration_ms: Some(1234),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Deserialization ────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn hand_deserializes_from_toml() {
|
||||
let toml_str = r#"
|
||||
name = "market-scanner"
|
||||
description = "Scans market trends"
|
||||
prompt = "Scan trends."
|
||||
|
||||
[schedule]
|
||||
kind = "cron"
|
||||
expr = "0 9 * * 1-5"
|
||||
tz = "America/New_York"
|
||||
"#;
|
||||
let hand: Hand = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(hand.name, "market-scanner");
|
||||
assert!(hand.active, "active should default to true");
|
||||
assert_eq!(hand.max_history, 100, "max_history should default to 100");
|
||||
assert!(hand.knowledge.is_empty());
|
||||
assert!(hand.allowed_tools.is_none());
|
||||
assert!(hand.model.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hand_deserializes_full_toml() {
|
||||
let toml_str = r#"
|
||||
name = "news-digest"
|
||||
description = "Daily news digest"
|
||||
prompt = "Summarize the day's news."
|
||||
knowledge = ["focus on AI", "include funding rounds"]
|
||||
allowed_tools = ["web_search"]
|
||||
model = "claude-opus-4-6"
|
||||
active = false
|
||||
max_history = 25
|
||||
|
||||
[schedule]
|
||||
kind = "every"
|
||||
every_ms = 3600000
|
||||
"#;
|
||||
let hand: Hand = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(hand.name, "news-digest");
|
||||
assert!(!hand.active);
|
||||
assert_eq!(hand.max_history, 25);
|
||||
assert_eq!(hand.knowledge.len(), 2);
|
||||
assert_eq!(hand.allowed_tools.as_ref().unwrap().len(), 1);
|
||||
assert_eq!(hand.model.as_deref(), Some("claude-opus-4-6"));
|
||||
assert!(matches!(
|
||||
hand.schedule,
|
||||
Schedule::Every {
|
||||
every_ms: 3_600_000
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hand_roundtrip_json() {
|
||||
let hand = sample_hand();
|
||||
let json = serde_json::to_string(&hand).unwrap();
|
||||
let parsed: Hand = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.name, hand.name);
|
||||
assert_eq!(parsed.max_history, hand.max_history);
|
||||
}
|
||||
|
||||
// ── HandRunStatus ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn hand_run_status_serde_roundtrip() {
|
||||
let statuses = vec![
|
||||
HandRunStatus::Running,
|
||||
HandRunStatus::Completed,
|
||||
HandRunStatus::Failed {
|
||||
error: "timeout".into(),
|
||||
},
|
||||
];
|
||||
for status in statuses {
|
||||
let json = serde_json::to_string(&status).unwrap();
|
||||
let parsed: HandRunStatus = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed, status);
|
||||
}
|
||||
}
|
||||
|
||||
// ── HandContext ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn context_new_is_empty() {
|
||||
let ctx = HandContext::new("test-hand");
|
||||
assert_eq!(ctx.hand_name, "test-hand");
|
||||
assert!(ctx.history.is_empty());
|
||||
assert!(ctx.learned_facts.is_empty());
|
||||
assert!(ctx.last_run.is_none());
|
||||
assert_eq!(ctx.total_runs, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_record_run_increments_counters() {
|
||||
let mut ctx = HandContext::new("scanner");
|
||||
let run = sample_run("scanner", HandRunStatus::Completed);
|
||||
ctx.record_run(run, 100);
|
||||
|
||||
assert_eq!(ctx.total_runs, 1);
|
||||
assert!(ctx.last_run.is_some());
|
||||
assert_eq!(ctx.history.len(), 1);
|
||||
assert_eq!(ctx.learned_facts, vec!["learned-fact-A"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_record_failed_run_does_not_increment_total() {
|
||||
let mut ctx = HandContext::new("scanner");
|
||||
let run = sample_run(
|
||||
"scanner",
|
||||
HandRunStatus::Failed {
|
||||
error: "boom".into(),
|
||||
},
|
||||
);
|
||||
ctx.record_run(run, 100);
|
||||
|
||||
assert_eq!(ctx.total_runs, 0);
|
||||
assert!(ctx.last_run.is_none());
|
||||
assert_eq!(ctx.history.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_caps_history_at_max() {
|
||||
let mut ctx = HandContext::new("scanner");
|
||||
for _ in 0..10 {
|
||||
let run = sample_run("scanner", HandRunStatus::Completed);
|
||||
ctx.record_run(run, 3);
|
||||
}
|
||||
assert_eq!(ctx.history.len(), 3);
|
||||
assert_eq!(ctx.total_runs, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_deduplicates_learned_facts() {
|
||||
let mut ctx = HandContext::new("scanner");
|
||||
let run1 = sample_run("scanner", HandRunStatus::Completed);
|
||||
let run2 = sample_run("scanner", HandRunStatus::Completed);
|
||||
ctx.record_run(run1, 100);
|
||||
ctx.record_run(run2, 100);
|
||||
|
||||
// Both runs add "learned-fact-A" but it should appear only once
|
||||
assert_eq!(ctx.learned_facts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_json_roundtrip() {
|
||||
let mut ctx = HandContext::new("scanner");
|
||||
let run = sample_run("scanner", HandRunStatus::Completed);
|
||||
ctx.record_run(run, 100);
|
||||
|
||||
let json = serde_json::to_string_pretty(&ctx).unwrap();
|
||||
let parsed: HandContext = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(parsed.hand_name, "scanner");
|
||||
assert_eq!(parsed.total_runs, 1);
|
||||
assert_eq!(parsed.history.len(), 1);
|
||||
assert_eq!(parsed.learned_facts, vec!["learned-fact-A"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn most_recent_run_is_first_in_history() {
|
||||
let mut ctx = HandContext::new("scanner");
|
||||
for i in 0..3 {
|
||||
let mut run = sample_run("scanner", HandRunStatus::Completed);
|
||||
run.findings = vec![format!("finding-{i}")];
|
||||
ctx.record_run(run, 100);
|
||||
}
|
||||
assert_eq!(ctx.history[0].findings[0], "finding-2");
|
||||
assert_eq!(ctx.history[2].findings[0], "finding-0");
|
||||
}
|
||||
}
|
||||
+399
-27
@@ -1,11 +1,75 @@
|
||||
use crate::config::HeartbeatConfig;
|
||||
use crate::observability::{Observer, ObserverEvent};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{self, Duration};
|
||||
use tracing::{info, warn};
|
||||
|
||||
// ── Structured task types ────────────────────────────────────────
|
||||
|
||||
/// Priority level for a heartbeat task.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TaskPriority {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
impl fmt::Display for TaskPriority {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Low => write!(f, "low"),
|
||||
Self::Medium => write!(f, "medium"),
|
||||
Self::High => write!(f, "high"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status of a heartbeat task.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TaskStatus {
|
||||
Active,
|
||||
Paused,
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl fmt::Display for TaskStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Active => write!(f, "active"),
|
||||
Self::Paused => write!(f, "paused"),
|
||||
Self::Completed => write!(f, "completed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structured heartbeat task with priority and status metadata.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HeartbeatTask {
|
||||
pub text: String,
|
||||
pub priority: TaskPriority,
|
||||
pub status: TaskStatus,
|
||||
}
|
||||
|
||||
impl HeartbeatTask {
|
||||
pub fn is_runnable(&self) -> bool {
|
||||
self.status == TaskStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HeartbeatTask {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "[{}] {}", self.priority, self.text)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Engine ───────────────────────────────────────────────────────
|
||||
|
||||
/// Heartbeat engine — reads HEARTBEAT.md and executes tasks periodically
|
||||
pub struct HeartbeatEngine {
|
||||
config: HeartbeatConfig,
|
||||
@@ -64,8 +128,8 @@ impl HeartbeatEngine {
|
||||
Ok(self.collect_tasks().await?.len())
|
||||
}
|
||||
|
||||
/// Read HEARTBEAT.md and return all parsed tasks.
|
||||
pub async fn collect_tasks(&self) -> Result<Vec<String>> {
|
||||
/// Read HEARTBEAT.md and return all parsed structured tasks.
|
||||
pub async fn collect_tasks(&self) -> Result<Vec<HeartbeatTask>> {
|
||||
let heartbeat_path = self.workspace_dir.join("HEARTBEAT.md");
|
||||
if !heartbeat_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
@@ -74,13 +138,145 @@ impl HeartbeatEngine {
|
||||
Ok(Self::parse_tasks(&content))
|
||||
}
|
||||
|
||||
/// Parse tasks from HEARTBEAT.md (lines starting with `- `)
|
||||
fn parse_tasks(content: &str) -> Vec<String> {
|
||||
/// Collect only runnable (active) tasks, sorted by priority (high first).
|
||||
pub async fn collect_runnable_tasks(&self) -> Result<Vec<HeartbeatTask>> {
|
||||
let mut tasks: Vec<HeartbeatTask> = self
|
||||
.collect_tasks()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(HeartbeatTask::is_runnable)
|
||||
.collect();
|
||||
// Sort by priority descending (High > Medium > Low)
|
||||
tasks.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
/// Parse tasks from HEARTBEAT.md with structured metadata support.
|
||||
///
|
||||
/// Supports both legacy flat format and new structured format:
|
||||
///
|
||||
/// Legacy:
|
||||
/// `- Check email` → medium priority, active status
|
||||
///
|
||||
/// Structured:
|
||||
/// `- [high] Check email` → high priority, active
|
||||
/// `- [low|paused] Review old PRs` → low priority, paused
|
||||
/// `- [completed] Old task` → medium priority, completed
|
||||
fn parse_tasks(content: &str) -> Vec<HeartbeatTask> {
|
||||
content
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let trimmed = line.trim();
|
||||
trimmed.strip_prefix("- ").map(ToString::to_string)
|
||||
let text = trimmed.strip_prefix("- ")?;
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Self::parse_task_line(text))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a single task line into a structured `HeartbeatTask`.
|
||||
///
|
||||
/// Format: `[priority|status] task text` or just `task text`.
|
||||
fn parse_task_line(text: &str) -> HeartbeatTask {
|
||||
if let Some(rest) = text.strip_prefix('[') {
|
||||
if let Some((meta, task_text)) = rest.split_once(']') {
|
||||
let task_text = task_text.trim();
|
||||
if !task_text.is_empty() {
|
||||
let (priority, status) = Self::parse_meta(meta);
|
||||
return HeartbeatTask {
|
||||
text: task_text.to_string(),
|
||||
priority,
|
||||
status,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// No metadata — default to medium/active
|
||||
HeartbeatTask {
|
||||
text: text.to_string(),
|
||||
priority: TaskPriority::Medium,
|
||||
status: TaskStatus::Active,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse metadata tags like `high`, `low|paused`, `completed`.
|
||||
fn parse_meta(meta: &str) -> (TaskPriority, TaskStatus) {
|
||||
let mut priority = TaskPriority::Medium;
|
||||
let mut status = TaskStatus::Active;
|
||||
|
||||
for part in meta.split('|') {
|
||||
match part.trim().to_ascii_lowercase().as_str() {
|
||||
"high" => priority = TaskPriority::High,
|
||||
"medium" | "med" => priority = TaskPriority::Medium,
|
||||
"low" => priority = TaskPriority::Low,
|
||||
"active" => status = TaskStatus::Active,
|
||||
"paused" | "pause" => status = TaskStatus::Paused,
|
||||
"completed" | "complete" | "done" => status = TaskStatus::Completed,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(priority, status)
|
||||
}
|
||||
|
||||
/// Build the Phase 1 LLM decision prompt for two-phase heartbeat.
|
||||
pub fn build_decision_prompt(tasks: &[HeartbeatTask]) -> String {
|
||||
let mut prompt = String::from(
|
||||
"You are a heartbeat scheduler. Review the following periodic tasks and decide \
|
||||
whether any should be executed right now.\n\n\
|
||||
Consider:\n\
|
||||
- Task priority (high tasks are more urgent)\n\
|
||||
- Whether the task is time-sensitive or can wait\n\
|
||||
- Whether running the task now would provide value\n\n\
|
||||
Tasks:\n",
|
||||
);
|
||||
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
use std::fmt::Write;
|
||||
let _ = writeln!(prompt, "{}. [{}] {}", i + 1, task.priority, task.text);
|
||||
}
|
||||
|
||||
prompt.push_str(
|
||||
"\nRespond with ONLY one of:\n\
|
||||
- `run: 1,2,3` (comma-separated task numbers to execute)\n\
|
||||
- `skip` (nothing needs to run right now)\n\n\
|
||||
Be conservative — skip if tasks are routine and not time-sensitive.",
|
||||
);
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
/// Parse the Phase 1 LLM decision response.
|
||||
///
|
||||
/// Returns indices of tasks to run, or empty vec if skipped.
|
||||
pub fn parse_decision_response(response: &str, task_count: usize) -> Vec<usize> {
|
||||
let trimmed = response.trim().to_ascii_lowercase();
|
||||
|
||||
if trimmed == "skip" || trimmed.starts_with("skip") {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Look for "run: 1,2,3" pattern
|
||||
let numbers_part = if let Some(after_run) = trimmed.strip_prefix("run:") {
|
||||
after_run.trim()
|
||||
} else if let Some(after_run) = trimmed.strip_prefix("run ") {
|
||||
after_run.trim()
|
||||
} else {
|
||||
// Try to parse as bare numbers
|
||||
trimmed.as_str()
|
||||
};
|
||||
|
||||
numbers_part
|
||||
.split(',')
|
||||
.filter_map(|s| {
|
||||
let n: usize = s.trim().parse().ok()?;
|
||||
if n >= 1 && n <= task_count {
|
||||
Some(n - 1) // Convert to 0-indexed
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -93,10 +289,14 @@ impl HeartbeatEngine {
|
||||
# Add tasks below (one per line, starting with `- `)\n\
|
||||
# The agent will check this file on each heartbeat tick.\n\
|
||||
#\n\
|
||||
# Format: - [priority|status] Task description\n\
|
||||
# priority: high, medium (default), low\n\
|
||||
# status: active (default), paused, completed\n\
|
||||
#\n\
|
||||
# Examples:\n\
|
||||
# - Check my email for important messages\n\
|
||||
# - [high] Check my email for important messages\n\
|
||||
# - Review my calendar for upcoming events\n\
|
||||
# - Check the weather forecast\n";
|
||||
# - [low|paused] Check the weather forecast\n";
|
||||
tokio::fs::write(&path, default).await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -112,9 +312,9 @@ mod tests {
|
||||
let content = "# Tasks\n\n- Check email\n- Review calendar\nNot a task\n- Third task";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 3);
|
||||
assert_eq!(tasks[0], "Check email");
|
||||
assert_eq!(tasks[1], "Review calendar");
|
||||
assert_eq!(tasks[2], "Third task");
|
||||
assert_eq!(tasks[0].text, "Check email");
|
||||
assert_eq!(tasks[0].priority, TaskPriority::Medium);
|
||||
assert_eq!(tasks[0].status, TaskStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -133,26 +333,21 @@ mod tests {
|
||||
let content = " - Indented task\n\t- Tab indented";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 2);
|
||||
assert_eq!(tasks[0], "Indented task");
|
||||
assert_eq!(tasks[1], "Tab indented");
|
||||
assert_eq!(tasks[0].text, "Indented task");
|
||||
assert_eq!(tasks[1].text, "Tab indented");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tasks_dash_without_space_ignored() {
|
||||
let content = "- Real task\n-\n- Another";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
// "-" trimmed = "-", does NOT start with "- " => skipped
|
||||
// "- Real task" => "Real task"
|
||||
// "- Another" => "Another"
|
||||
assert_eq!(tasks.len(), 2);
|
||||
assert_eq!(tasks[0], "Real task");
|
||||
assert_eq!(tasks[1], "Another");
|
||||
assert_eq!(tasks[0].text, "Real task");
|
||||
assert_eq!(tasks[1].text, "Another");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tasks_trailing_space_bullet_trimmed_to_dash() {
|
||||
// "- " trimmed becomes "-" (trim removes trailing space)
|
||||
// "-" does NOT start with "- " => skipped
|
||||
let content = "- ";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 0);
|
||||
@@ -160,11 +355,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_tasks_bullet_with_content_after_spaces() {
|
||||
// "- hello " trimmed becomes "- hello" => starts_with "- " => "hello"
|
||||
let content = "- hello ";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0], "hello");
|
||||
assert_eq!(tasks[0].text, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -172,8 +366,8 @@ mod tests {
|
||||
let content = "- Check email 📧\n- Review calendar 📅\n- 日本語タスク";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 3);
|
||||
assert!(tasks[0].contains("📧"));
|
||||
assert!(tasks[2].contains("日本語"));
|
||||
assert!(tasks[0].text.contains('📧'));
|
||||
assert!(tasks[2].text.contains("日本語"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -181,15 +375,15 @@ mod tests {
|
||||
let content = "# Periodic Tasks\n\n## Quick\n- Task A\n\n## Long\n- Task B\n\n* Not a dash bullet\n1. Not numbered";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 2);
|
||||
assert_eq!(tasks[0], "Task A");
|
||||
assert_eq!(tasks[1], "Task B");
|
||||
assert_eq!(tasks[0].text, "Task A");
|
||||
assert_eq!(tasks[1].text, "Task B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tasks_single_task() {
|
||||
let tasks = HeartbeatEngine::parse_tasks("- Only one");
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0], "Only one");
|
||||
assert_eq!(tasks[0].text, "Only one");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -201,9 +395,153 @@ mod tests {
|
||||
});
|
||||
let tasks = HeartbeatEngine::parse_tasks(&content);
|
||||
assert_eq!(tasks.len(), 100);
|
||||
assert_eq!(tasks[99], "Task 99");
|
||||
assert_eq!(tasks[99].text, "Task 99");
|
||||
}
|
||||
|
||||
// ── Structured task parsing tests ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn parse_task_with_high_priority() {
|
||||
let content = "- [high] Urgent email check";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].text, "Urgent email check");
|
||||
assert_eq!(tasks[0].priority, TaskPriority::High);
|
||||
assert_eq!(tasks[0].status, TaskStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_task_with_low_paused() {
|
||||
let content = "- [low|paused] Review old PRs";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].text, "Review old PRs");
|
||||
assert_eq!(tasks[0].priority, TaskPriority::Low);
|
||||
assert_eq!(tasks[0].status, TaskStatus::Paused);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_task_completed() {
|
||||
let content = "- [completed] Old task";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].priority, TaskPriority::Medium);
|
||||
assert_eq!(tasks[0].status, TaskStatus::Completed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_task_without_metadata_defaults() {
|
||||
let content = "- Plain task";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].text, "Plain task");
|
||||
assert_eq!(tasks[0].priority, TaskPriority::Medium);
|
||||
assert_eq!(tasks[0].status, TaskStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mixed_structured_and_legacy() {
|
||||
let content = "- [high] Urgent\n- Normal task\n- [low|paused] Later";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
assert_eq!(tasks.len(), 3);
|
||||
assert_eq!(tasks[0].priority, TaskPriority::High);
|
||||
assert_eq!(tasks[1].priority, TaskPriority::Medium);
|
||||
assert_eq!(tasks[2].priority, TaskPriority::Low);
|
||||
assert_eq!(tasks[2].status, TaskStatus::Paused);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runnable_filters_paused_and_completed() {
|
||||
let content = "- [high] Active\n- [low|paused] Paused\n- [completed] Done";
|
||||
let tasks = HeartbeatEngine::parse_tasks(content);
|
||||
let runnable: Vec<_> = tasks
|
||||
.into_iter()
|
||||
.filter(HeartbeatTask::is_runnable)
|
||||
.collect();
|
||||
assert_eq!(runnable.len(), 1);
|
||||
assert_eq!(runnable[0].text, "Active");
|
||||
}
|
||||
|
||||
// ── Two-phase decision tests ────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn decision_prompt_includes_all_tasks() {
|
||||
let tasks = vec![
|
||||
HeartbeatTask {
|
||||
text: "Check email".into(),
|
||||
priority: TaskPriority::High,
|
||||
status: TaskStatus::Active,
|
||||
},
|
||||
HeartbeatTask {
|
||||
text: "Review calendar".into(),
|
||||
priority: TaskPriority::Medium,
|
||||
status: TaskStatus::Active,
|
||||
},
|
||||
];
|
||||
let prompt = HeartbeatEngine::build_decision_prompt(&tasks);
|
||||
assert!(prompt.contains("1. [high] Check email"));
|
||||
assert!(prompt.contains("2. [medium] Review calendar"));
|
||||
assert!(prompt.contains("skip"));
|
||||
assert!(prompt.contains("run:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_decision_skip() {
|
||||
let indices = HeartbeatEngine::parse_decision_response("skip", 3);
|
||||
assert!(indices.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_decision_skip_with_reason() {
|
||||
let indices =
|
||||
HeartbeatEngine::parse_decision_response("skip — nothing urgent right now", 3);
|
||||
assert!(indices.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_decision_run_single() {
|
||||
let indices = HeartbeatEngine::parse_decision_response("run: 1", 3);
|
||||
assert_eq!(indices, vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_decision_run_multiple() {
|
||||
let indices = HeartbeatEngine::parse_decision_response("run: 1, 3", 3);
|
||||
assert_eq!(indices, vec![0, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_decision_run_out_of_range_ignored() {
|
||||
let indices = HeartbeatEngine::parse_decision_response("run: 1, 5, 2", 3);
|
||||
assert_eq!(indices, vec![0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_decision_run_zero_ignored() {
|
||||
let indices = HeartbeatEngine::parse_decision_response("run: 0, 1", 3);
|
||||
assert_eq!(indices, vec![0]);
|
||||
}
|
||||
|
||||
// ── Task display ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn task_display_format() {
|
||||
let task = HeartbeatTask {
|
||||
text: "Check email".into(),
|
||||
priority: TaskPriority::High,
|
||||
status: TaskStatus::Active,
|
||||
};
|
||||
assert_eq!(format!("{task}"), "[high] Check email");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn priority_ordering() {
|
||||
assert!(TaskPriority::High > TaskPriority::Medium);
|
||||
assert!(TaskPriority::Medium > TaskPriority::Low);
|
||||
}
|
||||
|
||||
// ── Async tests ─────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_heartbeat_file_creates_file() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_heartbeat");
|
||||
@@ -216,6 +554,7 @@ mod tests {
|
||||
assert!(path.exists());
|
||||
let content = tokio::fs::read_to_string(&path).await.unwrap();
|
||||
assert!(content.contains("Periodic Tasks"));
|
||||
assert!(content.contains("[high]"));
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
@@ -301,4 +640,37 @@ mod tests {
|
||||
let result = engine.run().await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collect_runnable_tasks_sorts_by_priority() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_runnable_sort");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
|
||||
tokio::fs::write(
|
||||
dir.join("HEARTBEAT.md"),
|
||||
"- [low] Low task\n- [high] High task\n- Medium task\n- [low|paused] Skip me",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let observer: Arc<dyn Observer> = Arc::new(crate::observability::NoopObserver);
|
||||
let engine = HeartbeatEngine::new(
|
||||
HeartbeatConfig {
|
||||
enabled: true,
|
||||
interval_minutes: 30,
|
||||
..HeartbeatConfig::default()
|
||||
},
|
||||
dir.clone(),
|
||||
observer,
|
||||
);
|
||||
|
||||
let tasks = engine.collect_runnable_tasks().await.unwrap();
|
||||
assert_eq!(tasks.len(), 3); // paused one excluded
|
||||
assert_eq!(tasks[0].priority, TaskPriority::High);
|
||||
assert_eq!(tasks[1].priority, TaskPriority::Medium);
|
||||
assert_eq!(tasks[2].priority, TaskPriority::Low);
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ fn show_integration_info(config: &Config, name: &str) -> Result<()> {
|
||||
|
||||
let Some(entry) = entries.iter().find(|e| e.name.to_lowercase() == name_lower) else {
|
||||
anyhow::bail!(
|
||||
"Unknown integration: {name}. Check README for supported integrations or run `zeroclaw onboard --interactive` to configure channels/providers."
|
||||
"Unknown integration: {name}. Check README for supported integrations or run `zeroclaw onboard` to configure channels/providers."
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+20
-6
@@ -48,6 +48,7 @@ pub(crate) mod cron;
|
||||
pub(crate) mod daemon;
|
||||
pub(crate) mod doctor;
|
||||
pub mod gateway;
|
||||
pub mod hands;
|
||||
pub(crate) mod hardware;
|
||||
pub(crate) mod health;
|
||||
pub(crate) mod heartbeat;
|
||||
@@ -280,15 +281,19 @@ Times are evaluated in UTC by default; use --tz with an IANA \
|
||||
timezone name to override.
|
||||
|
||||
Examples:
|
||||
zeroclaw cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York
|
||||
zeroclaw cron add '*/30 * * * *' 'Check system health'")]
|
||||
zeroclaw cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York --agent
|
||||
zeroclaw cron add '*/30 * * * *' 'Check system health' --agent
|
||||
zeroclaw cron add '*/5 * * * *' 'echo ok'")]
|
||||
Add {
|
||||
/// Cron expression
|
||||
expression: String,
|
||||
/// Optional IANA timezone (e.g. America/Los_Angeles)
|
||||
#[arg(long)]
|
||||
tz: Option<String>,
|
||||
/// Command to run
|
||||
/// Treat the argument as an agent prompt instead of a shell command
|
||||
#[arg(long)]
|
||||
agent: bool,
|
||||
/// Command (shell) or prompt (agent) to run
|
||||
command: String,
|
||||
},
|
||||
/// Add a one-shot scheduled task at an RFC3339 timestamp
|
||||
@@ -303,7 +308,10 @@ Examples:
|
||||
AddAt {
|
||||
/// One-shot timestamp in RFC3339 format
|
||||
at: String,
|
||||
/// Command to run
|
||||
/// Treat the argument as an agent prompt instead of a shell command
|
||||
#[arg(long)]
|
||||
agent: bool,
|
||||
/// Command (shell) or prompt (agent) to run
|
||||
command: String,
|
||||
},
|
||||
/// Add a fixed-interval scheduled task
|
||||
@@ -318,7 +326,10 @@ Examples:
|
||||
AddEvery {
|
||||
/// Interval in milliseconds
|
||||
every_ms: u64,
|
||||
/// Command to run
|
||||
/// Treat the argument as an agent prompt instead of a shell command
|
||||
#[arg(long)]
|
||||
agent: bool,
|
||||
/// Command (shell) or prompt (agent) to run
|
||||
command: String,
|
||||
},
|
||||
/// Add a one-shot delayed task (e.g. "30m", "2h", "1d")
|
||||
@@ -335,7 +346,10 @@ Examples:
|
||||
Once {
|
||||
/// Delay duration
|
||||
delay: String,
|
||||
/// Command to run
|
||||
/// Treat the argument as an agent prompt instead of a shell command
|
||||
#[arg(long)]
|
||||
agent: bool,
|
||||
/// Command (shell) or prompt (agent) to run
|
||||
command: String,
|
||||
},
|
||||
/// Remove a scheduled task
|
||||
|
||||
+87
-19
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
#![warn(clippy::all, clippy::pedantic)]
|
||||
#![allow(
|
||||
clippy::assigning_clones,
|
||||
@@ -46,6 +47,30 @@ fn parse_temperature(s: &str) -> std::result::Result<f64, String> {
|
||||
config::schema::validate_temperature(t)
|
||||
}
|
||||
|
||||
fn print_no_command_help() -> Result<()> {
|
||||
println!("No command provided.");
|
||||
println!("Try `zeroclaw onboard` to initialize your workspace.");
|
||||
println!();
|
||||
|
||||
let mut cmd = Cli::command();
|
||||
cmd.print_help()?;
|
||||
println!();
|
||||
|
||||
#[cfg(windows)]
|
||||
pause_after_no_command_help();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn pause_after_no_command_help() {
|
||||
println!();
|
||||
print!("Press Enter to exit...");
|
||||
let _ = std::io::stdout().flush();
|
||||
let mut line = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut line);
|
||||
}
|
||||
|
||||
mod agent;
|
||||
mod approval;
|
||||
mod auth;
|
||||
@@ -133,10 +158,6 @@ struct Cli {
|
||||
enum Commands {
|
||||
/// Initialize your workspace and configuration
|
||||
Onboard {
|
||||
/// Run the full interactive wizard (default is quick setup)
|
||||
#[arg(long)]
|
||||
interactive: bool,
|
||||
|
||||
/// Overwrite existing config without confirmation
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
@@ -145,11 +166,15 @@ enum Commands {
|
||||
#[arg(long)]
|
||||
reinit: bool,
|
||||
|
||||
/// Run the full interactive setup wizard
|
||||
#[arg(long)]
|
||||
interactive: bool,
|
||||
|
||||
/// Reconfigure channels only (fast repair flow)
|
||||
#[arg(long)]
|
||||
channels_only: bool,
|
||||
|
||||
/// API key (used in quick mode, ignored with --interactive)
|
||||
/// API key for provider configuration
|
||||
#[arg(long)]
|
||||
api_key: Option<String>,
|
||||
|
||||
@@ -304,11 +329,12 @@ override with --tz and an IANA timezone name.
|
||||
|
||||
Examples:
|
||||
zeroclaw cron list
|
||||
zeroclaw cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York
|
||||
zeroclaw cron add '*/30 * * * *' 'Check system health'
|
||||
zeroclaw cron add-at 2025-01-15T14:00:00Z 'Send reminder'
|
||||
zeroclaw cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York --agent
|
||||
zeroclaw cron add '*/30 * * * *' 'Check system health' --agent
|
||||
zeroclaw cron add '*/5 * * * *' 'echo ok'
|
||||
zeroclaw cron add-at 2025-01-15T14:00:00Z 'Send reminder' --agent
|
||||
zeroclaw cron add-every 60000 'Ping heartbeat'
|
||||
zeroclaw cron once 30m 'Run backup in 30 minutes'
|
||||
zeroclaw cron once 30m 'Run backup in 30 minutes' --agent
|
||||
zeroclaw cron pause <task-id>
|
||||
zeroclaw cron update <task-id> --expression '0 8 * * *' --tz Europe/London")]
|
||||
Cron {
|
||||
@@ -667,6 +693,10 @@ async fn main() -> Result<()> {
|
||||
eprintln!("Warning: Failed to install default crypto provider: {e:?}");
|
||||
}
|
||||
|
||||
if std::env::args_os().len() <= 1 {
|
||||
return print_no_command_help();
|
||||
}
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
if let Some(config_dir) = &cli.config_dir {
|
||||
@@ -698,9 +728,9 @@ async fn main() -> Result<()> {
|
||||
// Tokio runtime. To avoid "Cannot drop a runtime in a context where blocking is
|
||||
// not allowed", we run the wizard on a blocking thread via spawn_blocking.
|
||||
if let Commands::Onboard {
|
||||
interactive,
|
||||
force,
|
||||
reinit,
|
||||
interactive,
|
||||
channels_only,
|
||||
api_key,
|
||||
provider,
|
||||
@@ -708,23 +738,25 @@ async fn main() -> Result<()> {
|
||||
memory,
|
||||
} = &cli.command
|
||||
{
|
||||
let interactive = *interactive;
|
||||
let force = *force;
|
||||
let reinit = *reinit;
|
||||
let interactive = *interactive;
|
||||
let channels_only = *channels_only;
|
||||
let api_key = api_key.clone();
|
||||
let provider = provider.clone();
|
||||
let model = model.clone();
|
||||
let memory = memory.clone();
|
||||
|
||||
if interactive && channels_only {
|
||||
bail!("Use either --interactive or --channels-only, not both");
|
||||
}
|
||||
if reinit && channels_only {
|
||||
bail!("Use either --reinit or --channels-only, not both");
|
||||
bail!("--reinit and --channels-only cannot be used together");
|
||||
}
|
||||
if reinit && !interactive {
|
||||
bail!("--reinit requires --interactive mode");
|
||||
if interactive && channels_only {
|
||||
bail!("--interactive and --channels-only cannot be used together");
|
||||
}
|
||||
if interactive
|
||||
&& (api_key.is_some() || provider.is_some() || model.is_some() || memory.is_some())
|
||||
{
|
||||
bail!("--interactive does not accept --api-key, --provider, --model, or --memory");
|
||||
}
|
||||
if channels_only
|
||||
&& (api_key.is_some() || provider.is_some() || model.is_some() || memory.is_some())
|
||||
@@ -790,6 +822,33 @@ async fn main() -> Result<()> {
|
||||
)
|
||||
.await
|
||||
}?;
|
||||
|
||||
// Display pairing code — user enters it in the dashboard to pair securely.
|
||||
// The code is one-time use and brute-force protected (5 attempts → lockout).
|
||||
// No auth material is placed in URLs to prevent leakage via browser history,
|
||||
// Referer headers, clipboard, or proxy logs.
|
||||
if config.gateway.require_pairing {
|
||||
let pairing = security::PairingGuard::new(true, &config.gateway.paired_tokens);
|
||||
if let Some(code) = pairing.pairing_code() {
|
||||
println!();
|
||||
println!(" \x1b[1;34m🦀 Gateway Pairing Code\x1b[0m");
|
||||
println!();
|
||||
println!(" \x1b[1;34m┌──────────────┐\x1b[0m");
|
||||
println!(" \x1b[1;34m│\x1b[0m \x1b[1m{code}\x1b[0m \x1b[1;34m│\x1b[0m");
|
||||
println!(" \x1b[1;34m└──────────────┘\x1b[0m");
|
||||
println!();
|
||||
println!(" Enter this code in the dashboard to pair your device.");
|
||||
println!(" The code is single-use and expires after pairing.");
|
||||
println!();
|
||||
println!(
|
||||
" \x1b[2mDashboard: http://127.0.0.1:{}\x1b[0m",
|
||||
config.gateway.port
|
||||
);
|
||||
println!(" \x1b[2mDocs: https://www.zeroclawlabs.ai/docs\x1b[0m");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-start channels if user said yes during wizard
|
||||
if std::env::var("ZEROCLAW_AUTOSTART_CHANNELS").as_deref() == Ok("1") {
|
||||
channels::start_channels(config).await?;
|
||||
@@ -2112,7 +2171,6 @@ mod tests {
|
||||
|
||||
match cli.command {
|
||||
Commands::Onboard {
|
||||
interactive,
|
||||
force,
|
||||
channels_only,
|
||||
api_key,
|
||||
@@ -2120,7 +2178,6 @@ mod tests {
|
||||
model,
|
||||
..
|
||||
} => {
|
||||
assert!(!interactive);
|
||||
assert!(!force);
|
||||
assert!(!channels_only);
|
||||
assert_eq!(provider.as_deref(), Some("openrouter"));
|
||||
@@ -2166,6 +2223,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onboard_cli_accepts_interactive_flag() {
|
||||
let cli = Cli::try_parse_from(["zeroclaw", "onboard", "--interactive"])
|
||||
.expect("onboard --interactive should parse");
|
||||
|
||||
match cli.command {
|
||||
Commands::Onboard { interactive, .. } => assert!(interactive),
|
||||
other => panic!("expected onboard command, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_parses_estop_default_engage() {
|
||||
let cli = Cli::try_parse_from(["zeroclaw", "estop"]).expect("estop command should parse");
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
//! LLM-driven memory consolidation.
|
||||
//!
|
||||
//! After each conversation turn, extracts structured information:
|
||||
//! - `history_entry`: A timestamped summary for the daily conversation log.
|
||||
//! - `memory_update`: New facts, preferences, or decisions worth remembering
|
||||
//! long-term (or `null` if nothing new was learned).
|
||||
//!
|
||||
//! This two-phase approach replaces the naive raw-message auto-save with
|
||||
//! semantic extraction, similar to Nanobot's `save_memory` tool call pattern.
|
||||
|
||||
use crate::memory::traits::{Memory, MemoryCategory};
|
||||
use crate::providers::traits::Provider;
|
||||
|
||||
/// Output of consolidation extraction.
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConsolidationResult {
|
||||
/// Brief timestamped summary for the conversation history log.
|
||||
pub history_entry: String,
|
||||
/// New facts/preferences/decisions to store long-term, or None.
|
||||
pub memory_update: Option<String>,
|
||||
}
|
||||
|
||||
const CONSOLIDATION_SYSTEM_PROMPT: &str = r#"You are a memory consolidation engine. Given a conversation turn, extract:
|
||||
1. "history_entry": A brief summary of what happened in this turn (1-2 sentences). Include the key topic or action.
|
||||
2. "memory_update": Any NEW facts, preferences, decisions, or commitments worth remembering long-term. Return null if nothing new was learned.
|
||||
|
||||
Respond ONLY with valid JSON: {"history_entry": "...", "memory_update": "..." or null}
|
||||
Do not include any text outside the JSON object."#;
|
||||
|
||||
/// Run two-phase LLM-driven consolidation on a conversation turn.
|
||||
///
|
||||
/// Phase 1: Write a history entry to the Daily memory category.
|
||||
/// Phase 2: Write a memory update to the Core category (if the LLM identified new facts).
|
||||
///
|
||||
/// This function is designed to be called fire-and-forget via `tokio::spawn`.
|
||||
pub async fn consolidate_turn(
|
||||
provider: &dyn Provider,
|
||||
model: &str,
|
||||
memory: &dyn Memory,
|
||||
user_message: &str,
|
||||
assistant_response: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let turn_text = format!("User: {user_message}\nAssistant: {assistant_response}");
|
||||
|
||||
// Truncate very long turns to avoid wasting tokens on consolidation.
|
||||
// Use char-boundary-safe slicing to prevent panic on multi-byte UTF-8 (e.g. CJK text).
|
||||
let truncated = if turn_text.len() > 4000 {
|
||||
let mut end = 4000;
|
||||
while end > 0 && !turn_text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
format!("{}…", &turn_text[..end])
|
||||
} else {
|
||||
turn_text.clone()
|
||||
};
|
||||
|
||||
let raw = provider
|
||||
.chat_with_system(Some(CONSOLIDATION_SYSTEM_PROMPT), &truncated, model, 0.1)
|
||||
.await?;
|
||||
|
||||
let result: ConsolidationResult = parse_consolidation_response(&raw, &turn_text);
|
||||
|
||||
// Phase 1: Write history entry to Daily category.
|
||||
let date = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let history_key = format!("daily_{date}_{}", uuid::Uuid::new_v4());
|
||||
memory
|
||||
.store(
|
||||
&history_key,
|
||||
&result.history_entry,
|
||||
MemoryCategory::Daily,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Phase 2: Write memory update to Core category (if present).
|
||||
if let Some(ref update) = result.memory_update {
|
||||
if !update.trim().is_empty() {
|
||||
let mem_key = format!("core_{}", uuid::Uuid::new_v4());
|
||||
memory
|
||||
.store(&mem_key, update, MemoryCategory::Core, None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the LLM's consolidation response, with fallback for malformed JSON.
|
||||
fn parse_consolidation_response(raw: &str, fallback_text: &str) -> ConsolidationResult {
|
||||
// Try to extract JSON from the response (LLM may wrap in markdown code blocks).
|
||||
let cleaned = raw
|
||||
.trim()
|
||||
.trim_start_matches("```json")
|
||||
.trim_start_matches("```")
|
||||
.trim_end_matches("```")
|
||||
.trim();
|
||||
|
||||
serde_json::from_str(cleaned).unwrap_or_else(|_| {
|
||||
// Fallback: use truncated turn text as history entry.
|
||||
// Use char-boundary-safe slicing to prevent panic on multi-byte UTF-8.
|
||||
let summary = if fallback_text.len() > 200 {
|
||||
let mut end = 200;
|
||||
while end > 0 && !fallback_text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
format!("{}…", &fallback_text[..end])
|
||||
} else {
|
||||
fallback_text.to_string()
|
||||
};
|
||||
ConsolidationResult {
|
||||
history_entry: summary,
|
||||
memory_update: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_valid_json_response() {
|
||||
let raw = r#"{"history_entry": "User asked about Rust.", "memory_update": "User prefers Rust over Go."}"#;
|
||||
let result = parse_consolidation_response(raw, "fallback");
|
||||
assert_eq!(result.history_entry, "User asked about Rust.");
|
||||
assert_eq!(
|
||||
result.memory_update.as_deref(),
|
||||
Some("User prefers Rust over Go.")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_json_with_null_memory() {
|
||||
let raw = r#"{"history_entry": "Routine greeting.", "memory_update": null}"#;
|
||||
let result = parse_consolidation_response(raw, "fallback");
|
||||
assert_eq!(result.history_entry, "Routine greeting.");
|
||||
assert!(result.memory_update.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_json_wrapped_in_code_block() {
|
||||
let raw =
|
||||
"```json\n{\"history_entry\": \"Discussed deployment.\", \"memory_update\": null}\n```";
|
||||
let result = parse_consolidation_response(raw, "fallback");
|
||||
assert_eq!(result.history_entry, "Discussed deployment.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_on_malformed_response() {
|
||||
let raw = "I'm sorry, I can't do that.";
|
||||
let result = parse_consolidation_response(raw, "User: hello\nAssistant: hi");
|
||||
assert_eq!(result.history_entry, "User: hello\nAssistant: hi");
|
||||
assert!(result.memory_update.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_truncates_long_text() {
|
||||
let long_text = "x".repeat(500);
|
||||
let result = parse_consolidation_response("invalid", &long_text);
|
||||
// 200 bytes + "…" (3 bytes in UTF-8) = 203
|
||||
assert!(result.history_entry.len() <= 203);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_truncates_cjk_text_without_panic() {
|
||||
// Each CJK character is 3 bytes in UTF-8; byte index 200 may land
|
||||
// inside a character. This must not panic.
|
||||
let cjk_text = "二手书项目".repeat(50); // 250 chars = 750 bytes
|
||||
let result = parse_consolidation_response("invalid", &cjk_text);
|
||||
assert!(result
|
||||
.history_entry
|
||||
.is_char_boundary(result.history_entry.len()));
|
||||
assert!(result.history_entry.ends_with('…'));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod backend;
|
||||
pub mod chunker;
|
||||
pub mod cli;
|
||||
pub mod consolidation;
|
||||
pub mod embeddings;
|
||||
pub mod hygiene;
|
||||
pub mod lucid;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user