merge: integrate origin/main into zeroclaw_homecomming

Preserves all hardware additions (Aardvark, RPi, GPIO tools,
ToolRegistry, loader, manifest, subprocess modules) while
accepting main's 831-commit evolution across agent, channels,
security, tools, config, and infrastructure.

Conflict resolution strategy:
- Non-hardware files: accepted main's version (theirs)
- src/hardware/device.rs: kept Aardvark DeviceKind/DeviceRuntime variants
- src/hardware/mod.rs: kept aardvark, aardvark_tools, datasheet, rpi,
  loader, manifest, subprocess, tool_registry modules
- src/hardware/transport.rs: kept Aardvark TransportKind variant
- Cargo.toml: kept aardvark-sys workspace member and dependency
This commit is contained in:
ehushubhamshaw 2026-03-14 16:31:30 -04:00
commit 0aade407fa
364 changed files with 40843 additions and 9011 deletions

View File

@ -0,0 +1,19 @@
{
"arch": "arm",
"crt-static-defaults": true,
"data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64",
"emit-debug-gdb-scripts": false,
"env": "musl",
"executables": true,
"is-builtin": false,
"linker": "arm-linux-gnueabihf-gcc",
"linker-flavor": "gcc",
"llvm-target": "armv6-unknown-linux-musleabihf",
"max-atomic-width": 32,
"os": "linux",
"panic-strategy": "unwind",
"relocation-model": "static",
"target-endian": "little",
"target-pointer-width": "32",
"vendor": "unknown"
}

View File

@ -1,16 +1,33 @@
# macOS targets — pin minimum OS version so binaries run on supported releases.
# Intel (x86_64): target macOS 10.15 Catalina and later.
# Apple Silicon (aarch64): target macOS 11.0 Big Sur and later (no Catalina hardware exists).
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-mmacosx-version-min=10.15"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-mmacosx-version-min=11.0"]
[target.x86_64-unknown-linux-musl]
rustflags = ["-C", "link-arg=-static"]
[target.aarch64-unknown-linux-musl]
rustflags = ["-C", "link-arg=-static"]
# Raspberry Pi 3B/4B/5 — glibc linked, built with `cross` or brew aarch64 toolchain
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-unknown-linux-gnu-gcc"
# ARMv6 musl (Raspberry Pi Zero W)
[target.armv6l-unknown-linux-musleabihf]
rustflags = ["-C", "link-arg=-static"]
# Android targets (NDK toolchain)
# Android targets (Termux-native defaults).
# CI/NDK cross builds can override these via CARGO_TARGET_*_LINKER.
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi21-clang"
linker = "clang"
[target.aarch64-linux-android]
linker = "aarch64-linux-android21-clang"
linker = "clang"
# Windows targets — increase stack size for large JsonSchema derives
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "link-args=/STACK:8388608"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "link-args=/STACK:8388608"]

View File

@ -15,6 +15,9 @@ indent_size = 4
# Trailing whitespace is significant in Markdown (line breaks).
trim_trailing_whitespace = false
[*.go]
indent_style = tab
[*.{yml,yaml}]
indent_size = 2
@ -23,3 +26,7 @@ indent_size = 2
[Dockerfile]
indent_size = 4
[*.nix]
indent_style = space
indent_size = 2

50
.github/CODEOWNERS vendored
View File

@ -1,32 +1,32 @@
# Default owner for all files
* @chumyin
* @theonlyhennygod @JordanTheJet @chumyin
# Important functional modules
/src/agent/** @theonlyhennygod
/src/providers/** @theonlyhennygod
/src/channels/** @theonlyhennygod
/src/tools/** @theonlyhennygod
/src/gateway/** @theonlyhennygod
/src/runtime/** @theonlyhennygod
/src/memory/** @theonlyhennygod
/Cargo.toml @theonlyhennygod
/Cargo.lock @theonlyhennygod
/src/agent/** @theonlyhennygod @JordanTheJet @chumyin
/src/providers/** @theonlyhennygod @JordanTheJet @chumyin
/src/channels/** @theonlyhennygod @JordanTheJet @chumyin
/src/tools/** @theonlyhennygod @JordanTheJet @chumyin
/src/gateway/** @theonlyhennygod @JordanTheJet @chumyin
/src/runtime/** @theonlyhennygod @JordanTheJet @chumyin
/src/memory/** @theonlyhennygod @JordanTheJet @chumyin
/Cargo.toml @theonlyhennygod @JordanTheJet @chumyin
/Cargo.lock @theonlyhennygod @JordanTheJet @chumyin
# Security / tests / CI-CD ownership
/src/security/** @chumyin
/tests/** @chumyin
/.github/** @chumyin
/.github/workflows/** @chumyin
/.github/codeql/** @chumyin
/.github/dependabot.yml @chumyin
/SECURITY.md @chumyin
/docs/actions-source-policy.md @chumyin
/docs/ci-map.md @chumyin
/src/security/** @theonlyhennygod @JordanTheJet @chumyin
/tests/** @theonlyhennygod @JordanTheJet @chumyin
/.github/** @theonlyhennygod @JordanTheJet @chumyin
/.github/workflows/** @theonlyhennygod @JordanTheJet @chumyin
/.github/codeql/** @theonlyhennygod @JordanTheJet @chumyin
/.github/dependabot.yml @theonlyhennygod @JordanTheJet @chumyin
/SECURITY.md @theonlyhennygod @JordanTheJet @chumyin
/docs/actions-source-policy.md @theonlyhennygod @JordanTheJet @chumyin
/docs/ci-map.md @theonlyhennygod @JordanTheJet @chumyin
# Docs & governance
/docs/** @chumyin
/AGENTS.md @chumyin
/CLAUDE.md @chumyin
/CONTRIBUTING.md @chumyin
/docs/pr-workflow.md @chumyin
/docs/reviewer-playbook.md @chumyin
/docs/** @theonlyhennygod @JordanTheJet @chumyin
/AGENTS.md @theonlyhennygod @JordanTheJet @chumyin
/CLAUDE.md @theonlyhennygod @JordanTheJet @chumyin
/CONTRIBUTING.md @theonlyhennygod @JordanTheJet @chumyin
/docs/pr-workflow.md @theonlyhennygod @JordanTheJet @chumyin
/docs/reviewer-playbook.md @theonlyhennygod @JordanTheJet @chumyin

View File

@ -3,3 +3,10 @@ self-hosted-runner:
- Linux
- X64
- racknerd
- aws-india
- light
- cpu40
- codeql
- codeql-general
- blacksmith-2vcpu-ubuntu-2404
- blacksmith-8vcpu-ubuntu-2404

View File

@ -5,7 +5,7 @@ updates:
directory: "/"
schedule:
interval: daily
target-branch: dev
target-branch: main
open-pull-requests-limit: 3
labels:
- "dependencies"
@ -21,7 +21,7 @@ updates:
directory: "/"
schedule:
interval: daily
target-branch: dev
target-branch: main
open-pull-requests-limit: 1
labels:
- "ci"
@ -38,7 +38,7 @@ updates:
directory: "/"
schedule:
interval: daily
target-branch: dev
target-branch: main
open-pull-requests-limit: 1
labels:
- "ci"

View File

@ -2,7 +2,7 @@
Describe this PR in 2-5 bullets:
- Base branch target (`main`):
- Base branch target (`main` or `dev`; direct `main` PRs are allowed):
- Problem:
- Why it matters:
- What changed:
@ -28,7 +28,6 @@ Describe this PR in 2-5 bullets:
- Related #
- Depends on # (if stacked)
- Supersedes # (if replacing older PR)
- External tracking link(s) (optional):
## Supersede Attribution (required when `Supersedes #` is used)

View File

@ -23,7 +23,6 @@
"Nightly Summary & Routing"
],
"stable": [
"Main Promotion Gate",
"CI Required Gate",
"Security Audit",
"Feature Matrix Summary",

View File

@ -8,6 +8,7 @@
"zeroclaw-armv7-unknown-linux-gnueabihf.tar.gz",
"zeroclaw-armv7-linux-androideabi.tar.gz",
"zeroclaw-aarch64-linux-android.tar.gz",
"zeroclaw-x86_64-unknown-freebsd.tar.gz",
"zeroclaw-x86_64-apple-darwin.tar.gz",
"zeroclaw-aarch64-apple-darwin.tar.gz",
"zeroclaw-x86_64-pc-windows-msvc.zip"

View File

@ -5,21 +5,28 @@
"id": "RUSTSEC-2025-0141",
"owner": "repo-maintainers",
"reason": "Transitive via probe-rs in current release path; tracked for replacement when probe-rs updates.",
"ticket": "SEC-21",
"ticket": "RMN-21",
"expires_on": "2026-12-31"
},
{
"id": "RUSTSEC-2024-0384",
"owner": "repo-maintainers",
"reason": "Upstream rust-nostr advisory mitigation is still in progress; monitor until released fix lands.",
"ticket": "SEC-21",
"ticket": "RMN-21",
"expires_on": "2026-12-31"
},
{
"id": "RUSTSEC-2024-0388",
"owner": "repo-maintainers",
"reason": "Transitive via matrix-sdk indexeddb dependency chain in current matrix release line; track removal when upstream drops derivative.",
"ticket": "SEC-21",
"ticket": "RMN-21",
"expires_on": "2026-12-31"
},
{
"id": "RUSTSEC-2024-0436",
"owner": "repo-maintainers",
"reason": "Transitive via wasmtime dependency stack; tracked until upstream removes or replaces paste.",
"ticket": "RMN-21",
"expires_on": "2026-12-31"
}
]

View File

@ -5,35 +5,35 @@
"pattern": "src/security/leak_detector\\.rs",
"owner": "repo-maintainers",
"reason": "Fixture patterns are intentionally embedded for regression tests in leak detector logic.",
"ticket": "SEC-13",
"ticket": "RMN-13",
"expires_on": "2026-12-31"
},
{
"pattern": "src/agent/loop_\\.rs",
"owner": "repo-maintainers",
"reason": "Contains escaped template snippets used for command orchestration and parser coverage.",
"ticket": "SEC-13",
"ticket": "RMN-13",
"expires_on": "2026-12-31"
},
{
"pattern": "src/security/secrets\\.rs",
"owner": "repo-maintainers",
"reason": "Contains detector test vectors and redaction examples required for secret scanning tests.",
"ticket": "SEC-13",
"ticket": "RMN-13",
"expires_on": "2026-12-31"
},
{
"pattern": "docs/(i18n/vi/|vi/)?zai-glm-setup\\.md",
"owner": "repo-maintainers",
"reason": "Documentation contains literal environment variable placeholders for onboarding commands.",
"ticket": "SEC-13",
"ticket": "RMN-13",
"expires_on": "2026-12-31"
},
{
"pattern": "\\.github/workflows/pub-release\\.yml",
"owner": "repo-maintainers",
"reason": "Release workflow emits masked authorization header examples during registry smoke checks.",
"ticket": "SEC-13",
"ticket": "RMN-13",
"expires_on": "2026-12-31"
}
],
@ -42,14 +42,14 @@
"pattern": "Authorization: Bearer \\$\\{[^}]+\\}",
"owner": "repo-maintainers",
"reason": "Intentional placeholder used in docs/workflow snippets for safe header examples.",
"ticket": "SEC-13",
"ticket": "RMN-13",
"expires_on": "2026-12-31"
},
{
"pattern": "curl -sS -o /tmp/ghcr-release-manifest\\.json -w \"%\\{http_code\\}\"",
"owner": "repo-maintainers",
"reason": "Release smoke command string is non-secret telemetry and should not be flagged as credential leakage.",
"ticket": "SEC-13",
"ticket": "RMN-13",
"expires_on": "2026-12-31"
}
]

View File

@ -19,7 +19,6 @@ Workflow behavior documentation in this directory:
Current workflow helper scripts:
- `.github/workflows/scripts/ci_workflow_owner_approval.js`
- `.github/workflows/scripts/ci_license_file_owner_guard.js`
- `.github/workflows/scripts/lint_feedback.js`
- `.github/workflows/scripts/pr_auto_response_contributor_tier.js`

View File

@ -0,0 +1,83 @@
name: Auto Main Release Tag
on:
workflow_run:
workflows: ["Production Release Build"]
types: [completed]
branches: [main]
concurrency:
group: auto-main-release-tag
cancel-in-progress: false
permissions:
contents: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
tag:
name: Create release tag from Cargo.toml
if: github.event.workflow_run.conclusion == 'success'
runs-on: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Get version from Cargo.toml
id: version
shell: bash
run: |
set -euo pipefail
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*= *"//' | sed 's/"//')
if [[ -z "${VERSION}" ]]; then
echo "::error::Could not determine version from Cargo.toml"
exit 1
fi
echo "version=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "Detected version: v${VERSION}"
- name: Check if tag already exists
id: tag_check
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.version.outputs.version }}"
if git rev-parse "refs/tags/${TAG}" > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} already exists, skipping."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Tag ${TAG} does not exist, will create."
fi
- name: Create and push annotated tag
if: steps.tag_check.outputs.exists == 'false'
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"
echo "Created and pushed tag: ${TAG}"
echo "### Auto Release Tag" >> "$GITHUB_STEP_SUMMARY"
echo "- Tag: \`${TAG}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Triggered by: Production Release Build (run: ${{ github.event.workflow_run.id }})" >> "$GITHUB_STEP_SUMMARY"
- name: Tag already existed (skipped)
if: steps.tag_check.outputs.exists == 'true'
shell: bash
run: |
TAG="${{ steps.version.outputs.version }}"
echo "### Auto Release Tag (skipped)" >> "$GITHUB_STEP_SUMMARY"
echo "- Tag \`${TAG}\` already exists; no new tag created." >> "$GITHUB_STEP_SUMMARY"
echo "- To release a new version, bump the version in Cargo.toml before merging to main." >> "$GITHUB_STEP_SUMMARY"

View File

@ -1,61 +0,0 @@
name: CI Build (Fast)
# Optional fast release build that runs alongside the normal Build (Smoke) job.
# This workflow is informational and does not gate merges.
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
concurrency:
group: ci-fast-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
changes:
name: Detect Change Scope
runs-on: [self-hosted, Linux, X64]
outputs:
rust_changed: ${{ steps.scope.outputs.rust_changed }}
docs_only: ${{ steps.scope.outputs.docs_only }}
workflow_changed: ${{ steps.scope.outputs.workflow_changed }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Detect docs-only changes
id: scope
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
run: ./scripts/ci/detect_change_scope.sh
build-fast:
name: Build (Fast)
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 25
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: fast-build
cache-targets: true
- name: Build release binary
run: cargo build --release --locked --verbose

View File

@ -80,10 +80,16 @@ permissions:
contents: read
actions: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
canary-plan:
name: Canary Plan
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 20
outputs:
mode: ${{ steps.inputs.outputs.mode }}
@ -116,7 +122,8 @@ jobs:
trigger_rollback_on_abort="true"
rollback_branch="dev"
rollback_target_ref=""
fail_on_violation="true"
# Scheduled audits may not have live canary telemetry; report violations without failing by default.
fail_on_violation="false"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
mode="${{ github.event.inputs.mode || 'dry-run' }}"
@ -231,7 +238,7 @@ jobs:
name: Canary Execute
needs: [canary-plan]
if: github.event_name == 'workflow_dispatch' && needs.canary-plan.outputs.mode == 'execute' && needs.canary-plan.outputs.ready_to_execute == 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 10
permissions:
contents: write

View File

@ -1,16 +1,8 @@
name: CI/CD Change Audit
# Moved off PR path per CI/CD optimization PRD.
# Audit trail runs on push-to-main/dev only.
on:
pull_request:
branches: [dev, main]
paths:
- ".github/workflows/**"
- ".github/release/**"
- ".github/codeql/**"
- "scripts/ci/**"
- ".github/dependabot.yml"
- "deny.toml"
- ".gitleaks.toml"
push:
branches: [dev, main]
paths:
@ -35,16 +27,22 @@ on:
type: boolean
concurrency:
group: ci-change-audit-${{ github.event.pull_request.number || github.sha || github.run_id }}
group: ci-change-audit-${{ github.sha || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
audit:
name: CI Change Audit
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 15
steps:
- name: Checkout
@ -52,6 +50,12 @@ jobs:
with:
fetch-depth: 0
- name: Setup Python
shell: bash
run: |
set -euo pipefail
python3 --version
- name: Resolve base/head commits
id: refs
shell: bash
@ -59,7 +63,13 @@ jobs:
set -euo pipefail
head_sha="$(git rev-parse HEAD)"
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
base_sha="${{ github.event.pull_request.base.sha }}"
# For pull_request events, checkout uses refs/pull/*/merge; HEAD^1 is the
# effective base commit for this synthesized merge and avoids stale base.sha.
if git rev-parse --verify HEAD^1 >/dev/null 2>&1; then
base_sha="$(git rev-parse HEAD^1)"
else
base_sha="${{ github.event.pull_request.base.sha }}"
fi
elif [ "${GITHUB_EVENT_NAME}" = "push" ]; then
base_sha="${{ github.event.before }}"
else

View File

@ -1,68 +0,0 @@
name: Connectivity Probes (Legacy Wrapper)
on:
workflow_dispatch:
inputs:
enforcement_mode:
description: "enforce = fail when critical endpoints are unreachable; report-only = never fail run"
type: choice
required: false
default: enforce
options:
- enforce
- report-only
concurrency:
group: connectivity-probes-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
jobs:
probes:
name: Provider Connectivity Probes
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Legacy wrapper note
shell: bash
run: |
set -euo pipefail
{
echo "### Connectivity Probes (Legacy Wrapper)"
echo "- Preferred workflow: \`CI Provider Connectivity\`"
echo "- This run uses the shared endpoint-config probe engine."
} >> "$GITHUB_STEP_SUMMARY"
- name: Run provider connectivity matrix
shell: bash
env:
ENFORCEMENT_MODE: ${{ github.event.inputs.enforcement_mode || 'enforce' }}
run: |
set -euo pipefail
fail_on_critical="true"
if [ "${ENFORCEMENT_MODE}" = "report-only" ]; then
fail_on_critical="false"
fi
cmd=(python3 scripts/ci/provider_connectivity_matrix.py
--config .github/connectivity/providers.json
--output-json connectivity-report.json
--output-md connectivity-summary.md)
if [ "$fail_on_critical" = "true" ]; then
cmd+=(--fail-on-critical)
fi
"${cmd[@]}"
- name: Upload connectivity artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: connectivity-probes-${{ github.run_id }}
if-no-files-found: error
path: |
connectivity-report.json
connectivity-summary.md

View File

@ -0,0 +1,88 @@
---
name: Post-Release Validation
on:
release:
types: ["published"]
permissions:
contents: read
jobs:
validate:
name: Validate Published Release
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Download and verify release assets
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
echo "Validating release: ${RELEASE_TAG}"
# 1. Check release exists and is not draft
release_json="$(gh api \
"repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}")"
is_draft="$(echo "$release_json" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['draft'])")"
if [ "$is_draft" = "True" ]; then
echo "::warning::Release ${RELEASE_TAG} is still in draft."
fi
# 2. Check expected assets against artifact contract
asset_count="$(echo "$release_json" \
| python3 -c "import sys,json; print(len(json.load(sys.stdin)['assets']))")"
contract=".github/release/release-artifact-contract.json"
expected_count="$(python3 -c "
import json
c = json.load(open('$contract'))
total = sum(len(c[k]) for k in c if k != 'schema_version')
print(total)
")"
echo "Release has ${asset_count} assets (contract expects ${expected_count})"
if [ "$asset_count" -lt "$expected_count" ]; then
echo "::error::Expected >=${expected_count} release assets (from ${contract}), found ${asset_count}"
exit 1
fi
# 3. Download checksum file and one archive
gh release download "${RELEASE_TAG}" \
--pattern "SHA256SUMS" \
--dir /tmp/release-check
gh release download "${RELEASE_TAG}" \
--pattern "zeroclaw-x86_64-unknown-linux-gnu.tar.gz" \
--dir /tmp/release-check
# 4. Verify checksum
cd /tmp/release-check
if sha256sum --check --ignore-missing SHA256SUMS; then
echo "SHA256 checksum verification: passed"
else
echo "::error::SHA256 checksum verification failed"
exit 1
fi
# 5. Extract binary
tar xzf zeroclaw-x86_64-unknown-linux-gnu.tar.gz
- name: Smoke-test release binary
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
cd /tmp/release-check
if ./zeroclaw --version | grep -Fq "${RELEASE_TAG#v}"; then
echo "Binary version check: passed (${RELEASE_TAG})"
else
actual="$(./zeroclaw --version)"
echo "::error::Binary --version mismatch: ${actual}"
exit 1
fi
echo "Post-release validation: all checks passed"

View File

@ -30,10 +30,16 @@ concurrency:
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
probe:
name: Provider Connectivity Probe
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 20
steps:
- name: Checkout

152
.github/workflows/ci-queue-hygiene.yml vendored Normal file
View File

@ -0,0 +1,152 @@
name: CI Queue Hygiene
on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
inputs:
apply:
description: "Cancel selected queued runs (false = dry-run report only)"
required: true
default: false
type: boolean
status:
description: "Queued-run status scope"
required: true
default: queued
type: choice
options:
- queued
- in_progress
- requested
- waiting
max_cancel:
description: "Maximum runs to cancel in one execution"
required: true
default: "120"
type: string
concurrency:
group: ci-queue-hygiene
cancel-in-progress: false
permissions:
actions: write
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
hygiene:
name: Queue Hygiene
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Run queue hygiene policy
id: hygiene
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
mkdir -p artifacts
status_scope="queued"
max_cancel="120"
apply_mode="true"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
status_scope="${{ github.event.inputs.status || 'queued' }}"
max_cancel="${{ github.event.inputs.max_cancel || '120' }}"
apply_mode="${{ github.event.inputs.apply || 'false' }}"
fi
cmd=(python3 scripts/ci/queue_hygiene.py
--repo "${{ github.repository }}"
--status "${status_scope}"
--max-cancel "${max_cancel}"
--dedupe-workflow "CI Run"
--dedupe-workflow "Test E2E"
--dedupe-workflow "Docs Deploy"
--dedupe-workflow "PR Intake Checks"
--dedupe-workflow "PR Labeler"
--dedupe-workflow "PR Auto Responder"
--dedupe-workflow "Workflow Sanity"
--dedupe-workflow "PR Label Policy Check"
--priority-branch-prefix "release/"
--dedupe-include-non-pr
--non-pr-key branch
--output-json artifacts/queue-hygiene-report.json
--verbose)
if [ "${apply_mode}" = "true" ]; then
cmd+=(--apply)
fi
"${cmd[@]}"
{
echo "status_scope=${status_scope}"
echo "max_cancel=${max_cancel}"
echo "apply_mode=${apply_mode}"
} >> "$GITHUB_OUTPUT"
- name: Publish queue hygiene summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ ! -f artifacts/queue-hygiene-report.json ]; then
echo "Queue hygiene report not found." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
python3 - <<'PY'
from __future__ import annotations
import json
from pathlib import Path
report_path = Path("artifacts/queue-hygiene-report.json")
report = json.loads(report_path.read_text(encoding="utf-8"))
counts = report.get("counts", {})
results = report.get("results", {})
reasons = report.get("reason_counts", {})
lines = [
"### Queue Hygiene Report",
f"- Mode: `{report.get('mode', 'unknown')}`",
f"- Status scope: `{report.get('status_scope', 'queued')}`",
f"- Runs in scope: `{counts.get('runs_in_scope', 0)}`",
f"- Candidate runs before cap: `{counts.get('candidate_runs_before_cap', 0)}`",
f"- Candidate runs after cap: `{counts.get('candidate_runs_after_cap', 0)}`",
f"- Skipped by cap: `{counts.get('skipped_by_cap', 0)}`",
f"- Canceled: `{results.get('canceled', 0)}`",
f"- Cancel skipped (already terminal/conflict): `{results.get('skipped', 0)}`",
f"- Cancel failed: `{results.get('failed', 0)}`",
]
if reasons:
lines.append("")
lines.append("Reason counts:")
for reason, value in sorted(reasons.items()):
lines.append(f"- `{reason}`: `{value}`")
with Path("/tmp/queue-hygiene-summary.md").open("w", encoding="utf-8") as handle:
handle.write("\n".join(lines) + "\n")
PY
cat /tmp/queue-hygiene-summary.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload queue hygiene report
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: queue-hygiene-report
path: artifacts/queue-hygiene-report.json
if-no-files-found: ignore
retention-days: 14

View File

@ -1,5 +1,7 @@
name: CI Reproducible Build
# Moved off PR path per CI/CD optimization PRD.
# Reproducibility is a release concern; runs on push-to-main/dev + weekly schedule.
on:
push:
branches: [dev, main]
@ -8,16 +10,11 @@ on:
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/ensure_c_toolchain.sh"
- "scripts/ci/ensure_cargo_component.sh"
- "scripts/ci/ensure_cc.sh"
- "scripts/ci/reproducible_build_check.sh"
- ".github/workflows/ci-reproducible-build.yml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/reproducible_build_check.sh"
- "scripts/ci/self_heal_rust_toolchain.sh"
- ".github/workflows/ci-reproducible-build.yml"
schedule:
- cron: "45 5 * * 1" # Weekly Monday 05:45 UTC
@ -35,29 +32,59 @@ on:
type: boolean
concurrency:
group: repro-build-${{ github.event.pull_request.number || github.ref || github.run_id }}
group: repro-build-${{ github.ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
reproducibility:
name: Reproducible Build Probe
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 45
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 35
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Setup Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: zeroclaw-ci-v1
shared-key: ${{ runner.os }}-rust
cache-targets: true
cache-bin: false
- name: Run reproducible build check
shell: bash
run: |

View File

@ -48,17 +48,23 @@ on:
- cron: "15 7 * * 1" # Weekly Monday 07:15 UTC
concurrency:
group: ci-rollback-${{ github.event.inputs.branch || 'dev' }}
group: ci-rollback-${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.branch || 'dev') || github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
actions: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
rollback-plan:
name: Rollback Guard Plan
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 20
outputs:
branch: ${{ steps.plan.outputs.branch }}
@ -71,7 +77,7 @@ jobs:
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.branch || 'dev' }}
ref: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.branch || 'dev') || github.ref_name }}
- name: Build rollback plan
id: plan
@ -80,11 +86,12 @@ jobs:
set -euo pipefail
mkdir -p artifacts
branch_input="dev"
branch_input="${GITHUB_REF_NAME}"
mode_input="dry-run"
target_ref_input=""
allow_non_ancestor="false"
fail_on_violation="true"
# Scheduled audits can surface historical rollback violations; report without blocking by default.
fail_on_violation="false"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
branch_input="${{ github.event.inputs.branch || 'dev' }}"
@ -182,7 +189,7 @@ jobs:
name: Rollback Execute Actions
needs: [rollback-plan]
if: github.event_name == 'workflow_dispatch' && needs.rollback-plan.outputs.mode == 'execute' && needs.rollback-plan.outputs.ready_to_execute == 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 15
permissions:
contents: write

View File

@ -5,77 +5,87 @@ on:
branches: [dev, main]
pull_request:
branches: [dev, main]
merge_group:
branches: [dev, main]
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
group: ci-run-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
changes:
name: Detect Change Scope
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
outputs:
docs_only: ${{ steps.scope.outputs.docs_only }}
docs_changed: ${{ steps.scope.outputs.docs_changed }}
rust_changed: ${{ steps.scope.outputs.rust_changed }}
workflow_changed: ${{ steps.scope.outputs.workflow_changed }}
ci_cd_changed: ${{ steps.scope.outputs.ci_cd_changed }}
docs_files: ${{ steps.scope.outputs.docs_files }}
base_sha: ${{ steps.scope.outputs.base_sha }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 1
- name: Ensure diff base is available
shell: bash
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
run: |
set -euo pipefail
if [ -z "${BASE_SHA}" ]; then
echo "BASE_SHA is empty; detect_change_scope will use fallback mode."
exit 0
fi
if git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "BASE_SHA already present: ${BASE_SHA}"
exit 0
fi
echo "Fetching base commit ${BASE_SHA} for scope detection..."
if ! git fetch --no-tags --depth=1 origin "${BASE_SHA}"; then
echo "::warning::Unable to fetch BASE_SHA=${BASE_SHA}; detect_change_scope will use fallback mode."
fi
fetch-depth: 0
- name: Detect docs-only changes
id: scope
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event.before }}
run: ./scripts/ci/detect_change_scope.sh
lint:
name: Lint Gate (Format + Clippy + Strict Delta)
# --- Consolidated Rust quality gate ---
# Merges: lint, workspace-check, package-check into one job on a beefy runner.
# With shared cache, sequential steps on 8 vCPU is faster than 6 parallel 2 vCPU jobs.
quality-gate:
name: Quality Gate (Fmt + Clippy + Workspace + Package Checks)
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 25
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
components: rustfmt, clippy
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- name: Ensure cargo component
shell: bash
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: zeroclaw-ci-v1
shared-key: ${{ runner.os }}-rust
cache-targets: true
cache-bin: false
# Step 1: Format + Clippy (was: lint job)
- name: Run rust quality gate
run: ./scripts/ci/rust_quality_gate.sh
- name: Run strict lint delta gate
@ -83,44 +93,133 @@ jobs:
BASE_SHA: ${{ needs.changes.outputs.base_sha }}
run: ./scripts/ci/rust_strict_delta_gate.sh
test:
name: Test
needs: [changes, lint]
if: needs.changes.outputs.rust_changed == 'true' && needs.lint.result == 'success'
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Run tests
run: cargo test --locked --verbose
# Step 2: Workspace check (was: workspace-check job)
- name: Check workspace
run: cargo check --workspace --locked
build:
name: Build (Smoke)
# Step 3: Package checks (was: package-check matrix job)
- name: Check package zeroclaw-types
run: cargo check -p zeroclaw-types --locked
- name: Check package zeroclaw-core
run: cargo check -p zeroclaw-core --locked
# --- Consolidated test + build ---
# Merges: test, build into one job. Incremental from shared cache.
test-and-build:
name: Test + Build
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 20
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- name: Ensure cargo component
shell: bash
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: zeroclaw-ci-v1
shared-key: ${{ runner.os }}-rust
cache-targets: true
cache-bin: false
# Step 1: Tests with flake detection (was: test job)
- name: Run tests with flake detection
shell: bash
env:
BLOCK_ON_FLAKE: ${{ vars.CI_BLOCK_ON_FLAKE_SUSPECTED || 'false' }}
run: |
set -euo pipefail
mkdir -p artifacts
toolchain_bin=""
if [ -n "${CARGO:-}" ]; then
toolchain_bin="$(dirname "${CARGO}")"
elif [ -n "${RUSTC:-}" ]; then
toolchain_bin="$(dirname "${RUSTC}")"
fi
if [ -n "${toolchain_bin}" ] && [ -d "${toolchain_bin}" ]; then
case ":$PATH:" in
*":${toolchain_bin}:"*) ;;
*) export PATH="${toolchain_bin}:$PATH" ;;
esac
fi
if cargo test --locked --verbose; then
echo '{"flake_suspected":false,"status":"success"}' > artifacts/flake-probe.json
exit 0
fi
echo "::warning::First test run failed. Retrying for flake detection..."
if cargo test --locked --verbose; then
echo '{"flake_suspected":true,"status":"flake"}' > artifacts/flake-probe.json
echo "::warning::Flake suspected — test passed on retry"
if [ "${BLOCK_ON_FLAKE}" = "true" ]; then
echo "BLOCK_ON_FLAKE is set; failing on suspected flake."
exit 1
fi
exit 0
fi
echo '{"flake_suspected":false,"status":"failure"}' > artifacts/flake-probe.json
exit 1
- name: Publish flake probe summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/flake-probe.json ]; then
status=$(python3 -c "import json; print(json.load(open('artifacts/flake-probe.json'))['status'])")
flake=$(python3 -c "import json; print(json.load(open('artifacts/flake-probe.json'))['flake_suspected'])")
{
echo "### Test Flake Probe"
echo "- Status: \`${status}\`"
echo "- Flake suspected: \`${flake}\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload flake probe artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: test-flake-probe
path: artifacts/flake-probe.*
if-no-files-found: ignore
retention-days: 14
# Step 2: Release build + binary size check (was: build job)
- name: Build binary (smoke check)
run: cargo build --profile release-fast --locked --verbose
env:
CARGO_BUILD_JOBS: 8
CI_SMOKE_BUILD_ATTEMPTS: 3
run: bash scripts/ci/smoke_build_retry.sh
- name: Check binary size
env:
BINARY_SIZE_HARD_LIMIT_MB: 28
BINARY_SIZE_ADVISORY_MB: 20
BINARY_SIZE_TARGET_MB: 5
run: bash scripts/ci/check_binary_size.sh target/release-fast/zeroclaw
docs-only:
name: Docs-Only Fast Path
needs: [changes]
if: needs.changes.outputs.docs_only == 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Skip heavy jobs for docs-only change
run: echo "Docs-only change detected. Rust lint/test/build skipped."
@ -129,7 +228,7 @@ jobs:
name: Non-Rust Fast Path
needs: [changes]
if: needs.changes.outputs.docs_only != 'true' && needs.changes.outputs.rust_changed != 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Skip Rust jobs for non-Rust change scope
run: echo "No Rust-impacting files changed. Rust lint/test/build skipped."
@ -138,34 +237,16 @@ jobs:
name: Docs Quality
needs: [changes]
if: needs.changes.outputs.docs_changed == 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 1
- name: Ensure diff base is available
shell: bash
env:
BASE_SHA: ${{ needs.changes.outputs.base_sha }}
run: |
set -euo pipefail
if [ -z "${BASE_SHA}" ]; then
echo "BASE_SHA is empty; docs gate will fallback to full-file lint."
exit 0
fi
if git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "BASE_SHA already present: ${BASE_SHA}"
exit 0
fi
echo "Fetching base commit ${BASE_SHA} for docs diff..."
if ! git fetch --no-tags --depth=1 origin "${BASE_SHA}"; then
echo "::warning::Unable to fetch BASE_SHA=${BASE_SHA}; docs gate will fallback to full-file lint."
exit 0
fi
fetch-depth: 0
- name: Setup Node.js for markdown lint
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Markdown lint (changed lines only)
env:
@ -196,7 +277,7 @@ jobs:
- name: Link check (offline, added links only)
if: steps.collect_links.outputs.count != '0'
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
with:
fail: true
args: >-
@ -214,8 +295,8 @@ jobs:
lint-feedback:
name: Lint Feedback
if: github.event_name == 'pull_request'
needs: [changes, lint, docs-quality]
runs-on: [self-hosted, Linux, X64]
needs: [changes, quality-gate, docs-quality]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
permissions:
contents: read
pull-requests: write
@ -229,40 +310,19 @@ jobs:
env:
RUST_CHANGED: ${{ needs.changes.outputs.rust_changed }}
DOCS_CHANGED: ${{ needs.changes.outputs.docs_changed }}
LINT_RESULT: ${{ needs.lint.result }}
LINT_DELTA_RESULT: ${{ needs.lint.result }}
LINT_RESULT: ${{ needs.quality-gate.result }}
LINT_DELTA_RESULT: ${{ needs.quality-gate.result }}
DOCS_RESULT: ${{ needs.docs-quality.result }}
with:
script: |
const script = require('./.github/workflows/scripts/lint_feedback.js');
await script({github, context, core});
workflow-owner-approval:
name: Workflow Owner Approval
needs: [changes]
if: github.event_name == 'pull_request' && needs.changes.outputs.workflow_changed == 'true'
runs-on: [self-hosted, Linux, X64]
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Require owner approval for workflow file changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_OWNER_LOGINS: ${{ vars.WORKFLOW_OWNER_LOGINS }}
with:
script: |
const script = require('./.github/workflows/scripts/ci_workflow_owner_approval.js');
await script({ github, context, core });
license-file-owner-guard:
name: License File Owner Guard
needs: [changes]
if: github.event_name == 'pull_request'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
permissions:
contents: read
pull-requests: read
@ -276,11 +336,12 @@ jobs:
script: |
const script = require('./.github/workflows/scripts/ci_license_file_owner_guard.js');
await script({ github, context, core });
ci-required:
name: CI Required Gate
if: always()
needs: [changes, lint, test, build, docs-only, non-rust, docs-quality, lint-feedback, workflow-owner-approval, license-file-owner-guard]
runs-on: [self-hosted, Linux, X64]
needs: [changes, quality-gate, test-and-build, docs-only, non-rust, docs-quality, lint-feedback, license-file-owner-guard]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Enforce required status
shell: bash
@ -290,94 +351,57 @@ jobs:
event_name="${{ github.event_name }}"
rust_changed="${{ needs.changes.outputs.rust_changed }}"
docs_changed="${{ needs.changes.outputs.docs_changed }}"
workflow_changed="${{ needs.changes.outputs.workflow_changed }}"
docs_result="${{ needs.docs-quality.result }}"
workflow_owner_result="${{ needs.workflow-owner-approval.result }}"
license_owner_result="${{ needs.license-file-owner-guard.result }}"
if [ "${{ needs.changes.outputs.docs_only }}" = "true" ]; then
echo "workflow_owner_approval=${workflow_owner_result}"
echo "license_file_owner_guard=${license_owner_result}"
if [ "$event_name" = "pull_request" ] && [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$license_owner_result" != "success" ]; then
# --- Helper: enforce PR governance gates ---
check_pr_governance() {
if [ "$event_name" != "pull_request" ]; then return 0; fi
if [ "$license_owner_result" != "success" ]; then
echo "License file owner guard did not pass."
exit 1
fi
}
check_docs_quality() {
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Docs-only change detected, but docs-quality did not pass."
echo "Docs changed but docs-quality did not pass."
exit 1
fi
}
# --- Docs-only fast path ---
if [ "${{ needs.changes.outputs.docs_only }}" = "true" ]; then
check_pr_governance
check_docs_quality
echo "Docs-only fast path passed."
exit 0
fi
# --- Non-rust fast path ---
if [ "$rust_changed" != "true" ]; then
echo "rust_changed=false (non-rust fast path)"
echo "workflow_owner_approval=${workflow_owner_result}"
echo "license_file_owner_guard=${license_owner_result}"
if [ "$event_name" = "pull_request" ] && [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$license_owner_result" != "success" ]; then
echo "License file owner guard did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Non-rust change touched docs, but docs-quality did not pass."
exit 1
fi
check_pr_governance
check_docs_quality
echo "Non-rust fast path passed."
exit 0
fi
lint_result="${{ needs.lint.result }}"
lint_strict_delta_result="${{ needs.lint.result }}"
test_result="${{ needs.test.result }}"
build_result="${{ needs.build.result }}"
# --- Rust change path ---
quality_gate_result="${{ needs.quality-gate.result }}"
test_and_build_result="${{ needs.test-and-build.result }}"
echo "lint=${lint_result}"
echo "lint_strict_delta=${lint_strict_delta_result}"
echo "test=${test_result}"
echo "build=${build_result}"
echo "quality-gate=${quality_gate_result}"
echo "test-and-build=${test_and_build_result}"
echo "docs=${docs_result}"
echo "workflow_owner_approval=${workflow_owner_result}"
echo "license_file_owner_guard=${license_owner_result}"
if [ "$event_name" = "pull_request" ] && [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
check_pr_governance
if [ "$quality_gate_result" != "success" ] || [ "$test_and_build_result" != "success" ]; then
echo "Required CI jobs did not pass: quality-gate=${quality_gate_result} test-and-build=${test_and_build_result}"
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$license_owner_result" != "success" ]; then
echo "License file owner guard did not pass."
exit 1
fi
check_docs_quality
if [ "$event_name" = "pull_request" ]; then
if [ "$lint_result" != "success" ] || [ "$lint_strict_delta_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ]; then
echo "Required PR CI jobs did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "PR changed docs, but docs-quality did not pass."
exit 1
fi
echo "PR required checks passed."
exit 0
fi
if [ "$lint_result" != "success" ] || [ "$lint_strict_delta_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ]; then
echo "Required push CI jobs did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Push changed docs, but docs-quality did not pass."
exit 1
fi
echo "Push required checks passed."
echo "All required checks passed."

View File

@ -8,6 +8,7 @@ on:
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/ensure_cc.sh"
- "scripts/ci/generate_provenance.py"
- ".github/workflows/ci-supply-chain-provenance.yml"
workflow_dispatch:
@ -23,13 +24,16 @@ permissions:
id-token: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
provenance:
name: Build + Provenance Bundle
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 35
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -39,12 +43,51 @@ jobs:
with:
toolchain: 1.92.0
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- name: Activate toolchain binaries on PATH
shell: bash
run: |
set -euo pipefail
toolchain_bin="$(dirname "$(rustup which --toolchain 1.92.0 cargo)")"
echo "$toolchain_bin" >> "$GITHUB_PATH"
- name: Resolve host target
id: rust-meta
shell: bash
run: |
set -euo pipefail
host_target="$(rustup run 1.92.0 rustc -vV | sed -n 's/^host: //p')"
if [ -z "${host_target}" ]; then
echo "::error::Unable to resolve Rust host target."
exit 1
fi
echo "host_target=${host_target}" >> "$GITHUB_OUTPUT"
- name: Runner preflight (compiler + disk)
shell: bash
run: |
set -euo pipefail
./scripts/ci/ensure_cc.sh
echo "Runner: ${RUNNER_NAME:-unknown} (${RUNNER_OS:-unknown}/${RUNNER_ARCH:-unknown})"
free_kb="$(df -Pk . | awk 'NR==2 {print $4}')"
min_kb=$((10 * 1024 * 1024))
if [ "${free_kb}" -lt "${min_kb}" ]; then
echo "::error::Insufficient disk space on runner (<10 GiB free)."
df -h .
exit 1
fi
- name: Build release-fast artifact
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
host_target="$(rustc -vV | sed -n 's/^host: //p')"
host_target="${{ steps.rust-meta.outputs.host_target }}"
cargo build --profile release-fast --locked --target "$host_target"
cp "target/${host_target}/release-fast/zeroclaw" "artifacts/zeroclaw-${host_target}"
sha256sum "artifacts/zeroclaw-${host_target}" > "artifacts/zeroclaw-${host_target}.sha256"
@ -53,7 +96,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
host_target="${{ steps.rust-meta.outputs.host_target }}"
python3 scripts/ci/generate_provenance.py \
--artifact "artifacts/zeroclaw-${host_target}" \
--subject-name "zeroclaw-${host_target}" \
@ -66,7 +109,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
host_target="${{ steps.rust-meta.outputs.host_target }}"
statement="artifacts/provenance-${host_target}.intoto.json"
cosign sign-blob --yes \
--bundle="${statement}.sigstore.json" \
@ -78,7 +121,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
host_target="${{ steps.rust-meta.outputs.host_target }}"
python3 scripts/ci/emit_audit_event.py \
--event-type supply_chain_provenance \
--input-json "artifacts/provenance-${host_target}.intoto.json" \
@ -97,7 +140,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
host_target="${{ steps.rust-meta.outputs.host_target }}"
{
echo "### Supply Chain Provenance"
echo "- Target: \`${host_target}\`"

56
.github/workflows/deploy-web.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Deploy Web to GitHub Pages
on:
push:
branches: [main]
paths:
- 'web/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Install dependencies
working-directory: ./web
run: npm ci
- name: Build
working-directory: ./web
run: npm run build
- name: Setup Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
path: ./web/dist
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

View File

@ -41,16 +41,22 @@ on:
default: ""
concurrency:
group: docs-deploy-${{ github.event.pull_request.number || github.sha }}
group: docs-deploy-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
docs-quality:
name: Docs Quality Gate
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 20
outputs:
docs_files: ${{ steps.scope.outputs.docs_files }}
@ -67,6 +73,11 @@ jobs:
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Resolve docs diff scope
id: scope
shell: bash
@ -154,6 +165,11 @@ jobs:
if-no-files-found: ignore
retention-days: ${{ steps.deploy_guard.outputs.docs_guard_artifact_retention_days || 21 }}
- name: Setup Node.js for markdown lint
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Markdown quality gate
env:
BASE_SHA: ${{ steps.scope.outputs.base_sha }}
@ -178,7 +194,7 @@ jobs:
- name: Link check (added links)
if: github.event_name != 'workflow_dispatch' && steps.links.outputs.count != '0'
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
with:
fail: true
args: >-
@ -197,7 +213,7 @@ jobs:
name: Docs Preview Artifact
needs: [docs-quality]
if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_target == 'preview')
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -210,78 +226,13 @@ jobs:
mkdir -p site/docs
cp -R docs/. site/docs/
cp README.md site/README.md
cat > site/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ZeroClaw Docs Preview</title>
<style>
:root { color-scheme: light; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f7f8fb;
color: #1f2937;
}
.container {
max-width: 900px;
margin: 48px auto;
padding: 0 20px;
}
.card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
}
h1 { margin-top: 0; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
ul { line-height: 1.9; }
.muted { color: #6b7280; font-size: 14px; }
</style>
</head>
<body>
<main class="container">
<section class="card">
<h1>ZeroClaw Docs Preview</h1>
<p class="muted">Generated by <code>.github/workflows/docs-deploy.yml</code>.</p>
<ul>
<li><a href="./README.md">Repository README</a></li>
<li><a href="./docs/index.html">Docs Navigation</a></li>
<li><a href="./docs/README.md">Docs Home (Markdown)</a></li>
<li><a href="./docs/SUMMARY.md">Docs Summary (Markdown)</a></li>
</ul>
</section>
</main>
</body>
</html>
EOF
cat > site/docs/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ZeroClaw Docs Navigation</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 32px; color: #111827; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
ul { line-height: 1.9; }
</style>
</head>
<body>
<h1>ZeroClaw Docs Navigation</h1>
<ul>
<li><a href="../index.html">Back to site home</a></li>
<li><a href="./README.md">Docs Home</a></li>
<li><a href="./SUMMARY.md">Docs Summary</a></li>
</ul>
</body>
</html>
cat > site/index.md <<'EOF'
# ZeroClaw Docs Preview
This preview bundle is produced by `.github/workflows/docs-deploy.yml`.
- [Repository README](./README.md)
- [Docs Home](./docs/README.md)
EOF
- name: Upload preview artifact
@ -296,7 +247,7 @@ jobs:
name: Deploy Docs to GitHub Pages
needs: [docs-quality]
if: needs.docs-quality.outputs.deploy_target == 'production' && needs.docs-quality.outputs.ready_to_deploy == 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 20
permissions:
contents: read
@ -318,78 +269,13 @@ jobs:
mkdir -p site/docs
cp -R docs/. site/docs/
cp README.md site/README.md
cat > site/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ZeroClaw Documentation</title>
<style>
:root { color-scheme: light; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f7f8fb;
color: #1f2937;
}
.container {
max-width: 960px;
margin: 48px auto;
padding: 0 20px;
}
.card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
}
h1 { margin-top: 0; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
ul { line-height: 1.9; }
.muted { color: #6b7280; font-size: 14px; }
</style>
</head>
<body>
<main class="container">
<section class="card">
<h1>ZeroClaw Documentation</h1>
<p class="muted">Automatically deployed from <code>main</code> via <code>.github/workflows/docs-deploy.yml</code>.</p>
<ul>
<li><a href="./README.md">Repository README</a></li>
<li><a href="./docs/index.html">Docs Navigation</a></li>
<li><a href="./docs/README.md">Docs Home (Markdown)</a></li>
<li><a href="./docs/SUMMARY.md">Docs Summary (Markdown)</a></li>
</ul>
</section>
</main>
</body>
</html>
EOF
cat > site/docs/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ZeroClaw Docs Navigation</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 32px; color: #111827; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
ul { line-height: 1.9; }
</style>
</head>
<body>
<h1>ZeroClaw Docs Navigation</h1>
<ul>
<li><a href="../index.html">Back to site home</a></li>
<li><a href="./README.md">Docs Home</a></li>
<li><a href="./SUMMARY.md">Docs Summary</a></li>
</ul>
</body>
</html>
cat > site/index.md <<'EOF'
# ZeroClaw Documentation
This site is deployed automatically from `main` by `.github/workflows/docs-deploy.yml`.
- [Repository README](./README.md)
- [Docs Home](./docs/README.md)
EOF
- name: Publish deploy source summary

View File

@ -1,28 +1,21 @@
name: Feature Matrix
# Non-default feature lanes moved to nightly/weekly only per CI/CD optimization PRD.
# PR path only runs default lane via ci:full/ci:feature-matrix labels.
on:
push:
branches: [dev, main]
branches: [dev]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "tests/**"
- "scripts/ci/nightly_matrix_report.py"
- ".github/release/nightly-owner-routing.json"
- ".github/workflows/feature-matrix.yml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "tests/**"
- "scripts/ci/nightly_matrix_report.py"
- ".github/release/nightly-owner-routing.json"
- ".github/workflows/feature-matrix.yml"
types: [labeled]
merge_group:
branches: [dev, main]
schedule:
@ -52,12 +45,15 @@ permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
resolve-profile:
name: Resolve Matrix Profile
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
outputs:
profile: ${{ steps.resolve.outputs.profile }}
lane_job_prefix: ${{ steps.resolve.outputs.lane_job_prefix }}
@ -129,48 +125,89 @@ jobs:
feature-check:
name: ${{ needs.resolve-profile.outputs.lane_job_prefix }} (${{ matrix.name }})
needs: [resolve-profile]
runs-on: [self-hosted, Linux, X64]
if: >-
github.event_name != 'pull_request' ||
contains(github.event.pull_request.labels.*.name, 'ci:full') ||
contains(github.event.pull_request.labels.*.name, 'ci:feature-matrix')
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: ${{ fromJSON(needs.resolve-profile.outputs.lane_timeout_minutes) }}
strategy:
fail-fast: false
matrix:
include:
# Default lane: always runs (PR compile + nightly)
- name: default
compile_command: cargo check --locked
nightly_command: cargo test --locked --test agent_e2e --verbose
install_libudev: false
nightly_only: false
# Non-default lanes: nightly/weekly/dispatch only (skipped on PR compile profile)
- name: whatsapp-web
compile_command: cargo check --locked --no-default-features --features whatsapp-web
nightly_command: cargo check --locked --no-default-features --features whatsapp-web --verbose
install_libudev: false
nightly_only: true
- name: browser-native
compile_command: cargo check --locked --no-default-features --features browser-native
nightly_command: cargo check --locked --no-default-features --features browser-native --verbose
install_libudev: false
nightly_only: true
- name: nightly-all-features
compile_command: cargo check --locked --all-features
nightly_command: cargo test --locked --all-features --test agent_e2e --verbose
install_libudev: true
nightly_only: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
if: "!matrix.nightly_only || needs.resolve-profile.outputs.profile == 'nightly'"
with:
prefix-key: feature-matrix-${{ matrix.name }}
prefix-key: zeroclaw-ci-v1
shared-key: ${{ runner.os }}-rust
cache-targets: true
- name: Install Linux deps for all-features lane
if: matrix.install_libudev
- name: Ensure Linux deps for all-features lane
if: matrix.install_libudev && (!matrix.nightly_only || needs.resolve-profile.outputs.profile == 'nightly')
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libudev-dev pkg-config
set -euo pipefail
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libudev; then
echo "libudev development headers already available; skipping apt install."
exit 0
fi
echo "Installing missing libudev build dependencies..."
for attempt in 1 2 3; do
if sudo apt-get update -qq -o DPkg::Lock::Timeout=300 && \
sudo apt-get install -y --no-install-recommends --no-upgrade -o DPkg::Lock::Timeout=300 libudev-dev pkg-config; then
echo "Dependency installation succeeded on attempt ${attempt}."
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to install libudev-dev/pkg-config after ${attempt} attempts." >&2
exit 1
fi
echo "Dependency installation failed on attempt ${attempt}; retrying in 10s..."
sleep 10
done
- name: Skip non-default lane on compile profile
if: matrix.nightly_only && needs.resolve-profile.outputs.profile != 'nightly'
run: echo "Skipping non-default lane '${{ matrix.name }}' on compile profile."
- name: Run matrix lane command
if: "!matrix.nightly_only || needs.resolve-profile.outputs.profile == 'nightly'"
id: lane
shell: bash
run: |
@ -237,7 +274,7 @@ jobs:
echo "lane_exit_code=${status}" >> "$GITHUB_OUTPUT"
- name: Upload lane report
if: always()
if: always() && steps.lane.outcome != 'skipped'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ needs.resolve-profile.outputs.lane_artifact_prefix }}-${{ matrix.name }}
@ -246,7 +283,7 @@ jobs:
retention-days: ${{ fromJSON(needs.resolve-profile.outputs.lane_retention_days) }}
- name: Enforce lane success
if: steps.lane.outputs.lane_status != 'success'
if: steps.lane.outcome == 'success' && steps.lane.outputs.lane_status != 'success'
shell: bash
run: |
set -euo pipefail
@ -262,7 +299,7 @@ jobs:
name: ${{ needs.resolve-profile.outputs.summary_job_name }}
needs: [resolve-profile, feature-check]
if: always()
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

View File

@ -1,6 +1,6 @@
# Main Branch Delivery Flows
This document explains what runs when code is proposed to `dev`, promoted to `main`, and released.
This document explains what runs when code is proposed to `dev`/`main`, merged to `main`, and released.
Use this with:
@ -13,10 +13,10 @@ Use this with:
| Event | Main workflows |
| --- | --- |
| PR activity (`pull_request_target`) | `pr-intake-checks.yml`, `pr-labeler.yml`, `pr-auto-response.yml` |
| PR activity (`pull_request`) | `ci-run.yml`, `sec-audit.yml`, `main-promotion-gate.yml` (for `main` PRs), plus path-scoped workflows |
| PR activity (`pull_request`) | `ci-run.yml`, `sec-audit.yml`, plus path-scoped workflows |
| Push to `dev`/`main` | `ci-run.yml`, `sec-audit.yml`, plus path-scoped workflows |
| Tag push (`v*`) | `pub-release.yml` publish mode, `pub-docker-img.yml` publish job |
| Scheduled/manual | `pub-release.yml` verification mode, `sec-codeql.yml`, `feature-matrix.yml`, `test-fuzz.yml`, `pr-check-stale.yml`, `pr-check-status.yml`, `sync-contributors.yml`, `test-benchmarks.yml`, `test-e2e.yml` |
| Scheduled/manual | `pub-release.yml` verification mode, `sec-codeql.yml`, `feature-matrix.yml`, `test-fuzz.yml`, `pr-check-stale.yml`, `pr-check-status.yml`, `ci-queue-hygiene.yml`, `sync-contributors.yml`, `test-benchmarks.yml`, `test-e2e.yml` |
## Runtime and Docker Matrix
@ -76,12 +76,11 @@ Notes:
- `test`
- `flake-probe` (single-retry telemetry; optional block via `CI_BLOCK_ON_FLAKE_SUSPECTED`)
- `docs-quality`
7. If `.github/workflows/**` changed, `workflow-owner-approval` must pass.
8. If root license files (`LICENSE-APACHE`, `LICENSE-MIT`) changed, `license-file-owner-guard` allows only PR author `willsarg`.
9. `lint-feedback` posts actionable comment if lint/docs gates fail.
10. `CI Required Gate` aggregates results to final pass/fail.
11. Maintainer merges PR once checks and review policy are satisfied.
12. Merge emits a `push` event on `dev` (see scenario 4).
7. If root license files (`LICENSE-APACHE`, `LICENSE-MIT`) changed, `license-file-owner-guard` allows only PR author `willsarg`.
8. `lint-feedback` posts actionable comment if lint/docs gates fail.
9. `CI Required Gate` aggregates results to final pass/fail.
10. Maintainer merges PR once checks and review policy are satisfied.
11. Merge emits a `push` event on `dev` (see scenario 4).
### 2) PR from fork -> `dev`
@ -101,8 +100,8 @@ Notes:
4. Approval gate possibility:
- if Actions settings require maintainer approval for fork workflows, the `pull_request` run stays in `action_required`/waiting state until approved.
5. Event fan-out after labeling:
- `pr-labeler.yml` and manual label changes emit `labeled`/`unlabeled` events.
- those events retrigger `pull_request_target` automation (`pr-labeler.yml` and `pr-auto-response.yml`), creating extra run volume/noise.
- manual label changes emit `labeled`/`unlabeled` events.
- those events retrigger only label-driven `pull_request_target` automation (`pr-auto-response.yml`); `pr-labeler.yml` now runs only on PR lifecycle events (`opened`/`reopened`/`synchronize`/`ready_for_review`) to reduce churn.
6. When contributor pushes new commits to fork branch (`synchronize`):
- reruns: `pr-intake-checks.yml`, `pr-labeler.yml`, `ci-run.yml`, `sec-audit.yml`, and matching path-scoped PR workflows.
- does not rerun `pr-auto-response.yml` unless label/open events occur.
@ -110,30 +109,26 @@ Notes:
- `changes` computes `docs_only`, `docs_changed`, `rust_changed`, `workflow_changed`.
- `build` runs for Rust-impacting changes.
- `lint`/`lint-strict-delta`/`test`/`docs-quality` run on PR when `ci:full` label exists.
- `workflow-owner-approval` runs when `.github/workflows/**` changed.
- `CI Required Gate` emits final pass/fail for the PR head.
8. Fork PR merge blockers to check first when diagnosing stalls:
- run approval pending for fork workflows.
- `workflow-owner-approval` failing on workflow-file changes.
- `license-file-owner-guard` failing when root license files are modified by non-owner PR author.
- `CI Required Gate` failure caused by upstream jobs.
- repeated `pull_request_target` reruns from label churn causing noisy signals.
9. After merge, normal `push` workflows on `dev` execute (scenario 4).
### 3) Promotion PR `dev` -> `main`
### 3) PR to `main` (direct or from `dev`)
1. Maintainer opens PR with head `dev` and base `main`.
2. `main-promotion-gate.yml` runs and fails unless PR author is `willsarg` or `theonlyhennygod`.
3. `main-promotion-gate.yml` also fails if head repo/branch is not `<this-repo>:dev`.
4. `ci-run.yml` and `sec-audit.yml` run on the promotion PR.
5. Maintainer merges PR once checks and review policy pass.
6. Merge emits a `push` event on `main`.
1. Contributor or maintainer opens PR with base `main`.
2. `ci-run.yml` and `sec-audit.yml` run on the PR, plus any path-scoped workflows.
3. Maintainer merges PR once checks and review policy pass.
4. Merge emits a `push` event on `main`.
### 4) Push/Merge Queue to `dev` or `main` (including after merge)
1. Commit reaches `dev` or `main` (usually from a merged PR), or merge queue creates a `merge_group` validation commit.
2. `ci-run.yml` runs on `push` and `merge_group`.
3. `feature-matrix.yml` runs on `push` for Rust/workflow paths and on `merge_group`.
3. `feature-matrix.yml` runs on `push` to `dev` for Rust/workflow paths and on `merge_group`.
4. `sec-audit.yml` runs on `push` and `merge_group`.
5. `sec-codeql.yml` runs on `push`/`merge_group` when Rust/codeql paths change (path-scoped on push).
6. `ci-supply-chain-provenance.yml` runs on push when Rust/build provenance paths change.
@ -151,7 +146,7 @@ Workflow: `.github/workflows/pub-docker-img.yml`
1. Triggered on `pull_request` to `dev` or `main` when Docker build-input paths change.
2. Runs `PR Docker Smoke` job:
- Builds local smoke image with Blacksmith builder.
- Builds local smoke image with Buildx builder.
- Verifies container with `docker run ... --version`.
3. Typical runtime in recent sample: ~240.4s.
4. No registry push happens on PR events.
@ -204,9 +199,8 @@ Canary policy lane:
## Merge/Policy Notes
1. Workflow-file changes (`.github/workflows/**`) activate owner-approval gate in `ci-run.yml`.
1. Workflow-file changes (`.github/workflows/**`) are validated through `pr-intake-checks.yml`, `ci-change-audit.yml`, and `CI Required Gate` without a dedicated owner-approval gate.
2. PR lint/test strictness is intentionally controlled by `ci:full` label.
3. `pr-intake-checks.yml` validates PR-template completeness and patch safety hints; no external tracker key is required.
4. `sec-audit.yml` runs on PR/push/merge queue (`merge_group`), plus scheduled weekly.
5. `ci-change-audit.yml` enforces pinned `uses:` references for CI/security workflow changes.
6. `sec-audit.yml` includes deny policy hygiene checks (`deny_policy_guard.py`) before cargo-deny.
@ -216,14 +210,15 @@ Canary policy lane:
10. Workflow-specific JavaScript helpers are organized under `.github/workflows/scripts/`.
11. `ci-run.yml` includes cache partitioning (`prefix-key`) across lint/test/build/flake-probe lanes to reduce cache contention.
12. `ci-rollback.yml` provides a guarded rollback planning lane (scheduled dry-run + manual execute controls) with audit artifacts.
13. `ci-queue-hygiene.yml` periodically deduplicates superseded queued runs for lightweight PR automation workflows to reduce queue pressure.
## Mermaid Diagrams
### PR to Main
### PR to Dev
```mermaid
flowchart TD
A["PR opened or updated -> main"] --> B["pull_request_target lane"]
A["PR opened or updated -> dev"] --> B["pull_request_target lane"]
B --> B1["pr-intake-checks.yml"]
B --> B2["pr-labeler.yml"]
B --> B3["pr-auto-response.yml"]
@ -237,32 +232,32 @@ flowchart TD
D --> E{"Checks + review policy pass?"}
E -->|No| F["PR stays open"]
E -->|Yes| G["Merge PR"]
G --> H["push event on main"]
G --> H["push event on dev"]
```
### Promotion and Release
### Main Delivery and Release
```mermaid
flowchart TD
D0["Commit reaches dev"] --> B0["ci-run.yml"]
D0 --> C0["sec-audit.yml"]
P["PR to main"] --> PG["main-promotion-gate.yml"]
PG --> M["Merge to main"]
PRM["PR to main"] --> QM["ci-run.yml + sec-audit.yml (+ path-scoped)"]
QM --> M["Merge to main"]
M --> A["Commit reaches main"]
A --> B["ci-run.yml"]
A --> C["sec-audit.yml"]
A --> D["path-scoped workflows (if matched)"]
T["Tag push v*"] --> R["pub-release.yml"]
W["Manual/Scheduled release verify"] --> R
T --> P["pub-docker-img.yml publish job"]
T --> DP["pub-docker-img.yml publish job"]
R --> R1["Artifacts + SBOM + checksums + signatures + GitHub Release"]
W --> R2["Verification build only (no GitHub Release publish)"]
P --> P1["Push ghcr image tags (version + sha + latest)"]
DP --> P1["Push ghcr image tags (version + sha + latest)"]
```
## Quick Troubleshooting
1. Unexpected skipped jobs: inspect `scripts/ci/detect_change_scope.sh` outputs.
2. Workflow-change PR blocked: verify `WORKFLOW_OWNER_LOGINS` and approvals.
2. CI/CD-change PR blocked: verify `@chumyin` approved review is present.
3. Fork PR appears stalled: check whether Actions run approval is pending.
4. Docker not published: confirm a `v*` tag was pushed to the intended commit.

View File

@ -1,34 +0,0 @@
name: Main Promotion Gate
on:
pull_request:
branches: [main]
concurrency:
group: main-promotion-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
jobs:
enforce-dev-promotion:
name: Enforce Dev -> Main Promotion
runs-on: [self-hosted, Linux, X64]
steps:
- name: Validate main PR metadata
shell: bash
env:
HEAD_REF: ${{ github.head_ref }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
BASE_REPO: ${{ github.repository }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail
if [[ -z "${PR_AUTHOR}" || -z "${HEAD_REF}" ]]; then
echo "::error::Missing PR metadata (author/head_ref)."
exit 1
fi
echo "Main PR policy satisfied: author=${PR_AUTHOR}, source=${HEAD_REPO}:${HEAD_REF} -> main"

View File

@ -19,12 +19,15 @@ permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
nightly-lanes:
name: Nightly Lane (${{ matrix.name }})
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 70
strategy:
fail-fast: false
@ -50,22 +53,42 @@ jobs:
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: nightly-all-features-${{ matrix.name }}
- name: Install Linux deps for all-features lane
- name: Ensure Linux deps for all-features lane
if: matrix.install_libudev
shell: bash
run: |
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libudev-dev pkg-config
else
apt-get update -qq
apt-get install -y --no-install-recommends libudev-dev pkg-config
set -euo pipefail
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libudev; then
echo "libudev development headers already available; skipping apt install."
exit 0
fi
echo "Installing missing libudev build dependencies..."
for attempt in 1 2 3; do
if sudo apt-get update -qq -o DPkg::Lock::Timeout=300 && \
sudo apt-get install -y --no-install-recommends --no-upgrade -o DPkg::Lock::Timeout=300 libudev-dev pkg-config; then
echo "Dependency installation succeeded on attempt ${attempt}."
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to install libudev-dev/pkg-config after ${attempt} attempts." >&2
exit 1
fi
echo "Dependency installation failed on attempt ${attempt}; retrying in 10s..."
sleep 10
done
- name: Run nightly lane command
id: lane
shell: bash
@ -119,7 +142,7 @@ jobs:
name: Nightly Summary & Routing
needs: [nightly-lanes]
if: always()
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

View File

@ -22,7 +22,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
steps:
- name: Checkout
@ -53,7 +53,7 @@ jobs:
deploy:
needs: build
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

View File

@ -7,19 +7,27 @@ on:
branches: [dev, main]
types: [opened, labeled, unlabeled]
concurrency:
# Keep cancellation within the same lifecycle action to avoid `labeled`
# events canceling an in-flight `opened` run for the same issue/PR.
group: pr-auto-response-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}-${{ github.event.action || 'unknown' }}
cancel-in-progress: true
permissions: {}
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
LABEL_POLICY_PATH: .github/label-policy.json
jobs:
contributor-tier-issues:
# Only run for opened/reopened events to avoid duplicate runs with labeled-routes job
if: >-
(github.event_name == 'issues' &&
(github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')) ||
(github.event_name == 'pull_request_target' &&
(github.event.action == 'labeled' || github.event.action == 'unlabeled'))
runs-on: [self-hosted, Linux, X64]
(github.event.action == 'opened' || github.event.action == 'reopened'))
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
permissions:
contents: read
issues: write
@ -38,7 +46,7 @@ jobs:
await script({ github, context, core });
first-interaction:
if: github.event.action == 'opened'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
permissions:
issues: write
pull-requests: write
@ -69,7 +77,7 @@ jobs:
labeled-routes:
if: github.event.action == 'labeled'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
permissions:
contents: read
issues: write

View File

@ -7,12 +7,18 @@ on:
permissions: {}
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 10
steps:
- name: Mark stale issues and pull requests
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0

View File

@ -11,9 +11,15 @@ concurrency:
group: pr-check-status
cancel-in-progress: true
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
nudge-stale-prs:
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
@ -23,7 +29,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Nudge PRs that need rebase or CI refresh
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:

View File

@ -3,7 +3,7 @@ name: PR Intake Checks
on:
pull_request_target:
branches: [dev, main]
types: [opened, reopened, synchronize, edited, ready_for_review]
types: [opened, reopened, synchronize, ready_for_review]
concurrency:
group: pr-intake-checks-${{ github.event.pull_request.number || github.run_id }}
@ -14,10 +14,16 @@ permissions:
pull-requests: write
issues: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
intake:
name: Intake Checks
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 10
steps:
- name: Checkout repository

View File

@ -7,6 +7,7 @@ on:
- ".github/workflows/pr-labeler.yml"
- ".github/workflows/pr-auto-response.yml"
push:
branches: [dev, main]
paths:
- ".github/label-policy.json"
- ".github/workflows/pr-labeler.yml"
@ -19,9 +20,15 @@ concurrency:
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
contributor-tier-consistency:
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 10
steps:
- name: Checkout

View File

@ -3,7 +3,7 @@ name: PR Labeler
on:
pull_request_target:
branches: [dev, main]
types: [opened, reopened, synchronize, edited, labeled, unlabeled]
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
inputs:
mode:
@ -25,11 +25,14 @@ permissions:
issues: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
LABEL_POLICY_PATH: .github/label-policy.json
jobs:
label:
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

View File

@ -17,20 +17,28 @@ on:
- "scripts/ci/ghcr_publish_contract_guard.py"
- "scripts/ci/ghcr_vulnerability_gate.py"
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to publish (e.g. v0.2.0). Leave empty for smoke-only run."
required: false
type: string
concurrency:
group: docker-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
pr-smoke:
name: PR Docker Smoke
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
runs-on: [self-hosted, Linux, X64]
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name == 'workflow_dispatch' && inputs.release_tag == '')
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 25
permissions:
contents: read
@ -38,8 +46,22 @@ jobs:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Buildx Builder
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v1
- name: Resolve Docker API version
shell: bash
run: |
set -euo pipefail
server_api="$(docker version --format '{{.Server.APIVersion}}')"
min_api="$(docker version --format '{{.Server.MinAPIVersion}}' 2>/dev/null || true)"
if [[ -z "${server_api}" || "${server_api}" == "<no value>" ]]; then
echo "::error::Unable to detect Docker server API version."
docker version || true
exit 1
fi
echo "DOCKER_API_VERSION=${server_api}" >> "$GITHUB_ENV"
echo "Using Docker API version ${server_api} (server min: ${min_api:-unknown})"
- name: Setup Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Extract metadata (tags, labels)
if: github.event_name == 'pull_request'
@ -51,7 +73,7 @@ jobs:
type=ref,event=pr
- name: Build smoke image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v2
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
push: false
@ -69,11 +91,9 @@ jobs:
publish:
name: Build and Push Docker Image
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'zeroclaw-labs/zeroclaw'
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 45
environment:
name: release
if: github.repository == 'zeroclaw-labs/zeroclaw' && ((github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && inputs.release_tag != ''))
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 90
permissions:
contents: read
packages: write
@ -81,9 +101,25 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.release_tag) || github.ref }}
- name: Setup Buildx Builder
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v1
- name: Resolve Docker API version
shell: bash
run: |
set -euo pipefail
server_api="$(docker version --format '{{.Server.APIVersion}}')"
min_api="$(docker version --format '{{.Server.MinAPIVersion}}' 2>/dev/null || true)"
if [[ -z "${server_api}" || "${server_api}" == "<no value>" ]]; then
echo "::error::Unable to detect Docker server API version."
docker version || true
exit 1
fi
echo "DOCKER_API_VERSION=${server_api}" >> "$GITHUB_ENV"
echo "Using Docker API version ${server_api} (server min: ${min_api:-unknown})"
- name: Setup Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
@ -98,31 +134,53 @@ jobs:
run: |
set -euo pipefail
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
SHA_SUFFIX="sha-${GITHUB_SHA::12}"
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
echo "::error::Docker publish is restricted to v* tag pushes."
exit 1
fi
RELEASE_TAG="${GITHUB_REF#refs/tags/}"
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
RELEASE_TAG="${{ inputs.release_tag }}"
if [[ -z "${RELEASE_TAG}" ]]; then
echo "::error::workflow_dispatch publish requires inputs.release_tag"
exit 1
fi
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::release_tag must be vX.Y.Z or vX.Y.Z-suffix (received: ${RELEASE_TAG})"
exit 1
fi
if ! git rev-parse --verify "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
echo "::error::release tag not found in checkout: ${RELEASE_TAG}"
exit 1
fi
else
echo "::error::Unsupported event for publish: ${GITHUB_EVENT_NAME}"
exit 1
fi
RELEASE_SHA="$(git rev-parse HEAD)"
SHA_SUFFIX="sha-${RELEASE_SHA::12}"
SHA_TAG="${IMAGE}:${SHA_SUFFIX}"
LATEST_SUFFIX="latest"
LATEST_TAG="${IMAGE}:${LATEST_SUFFIX}"
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
echo "::error::Docker publish is restricted to v* tag pushes."
exit 1
fi
RELEASE_TAG="${GITHUB_REF#refs/tags/}"
VERSION_TAG="${IMAGE}:${RELEASE_TAG}"
TAGS="${VERSION_TAG},${SHA_TAG},${LATEST_TAG}"
{
echo "tags=${TAGS}"
echo "release_tag=${RELEASE_TAG}"
echo "release_sha=${RELEASE_SHA}"
echo "sha_tag=${SHA_SUFFIX}"
echo "latest_tag=${LATEST_SUFFIX}"
} >> "$GITHUB_OUTPUT"
- name: Build and push Docker image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v2
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
push: true
build-args: |
ZEROCLAW_CARGO_ALL_FEATURES=true
tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
@ -172,7 +230,7 @@ jobs:
python3 scripts/ci/ghcr_publish_contract_guard.py \
--repository "${GITHUB_REPOSITORY,,}" \
--release-tag "${{ steps.meta.outputs.release_tag }}" \
--sha "${GITHUB_SHA}" \
--sha "${{ steps.meta.outputs.release_sha }}" \
--policy-file .github/release/ghcr-tag-policy.json \
--output-json artifacts/ghcr-publish-contract.json \
--output-md artifacts/ghcr-publish-contract.md \
@ -327,11 +385,25 @@ jobs:
if-no-files-found: ignore
retention-days: 21
- name: Upload Trivy SARIF
- name: Detect Trivy SARIF report
id: trivy-sarif
if: always()
shell: bash
run: |
set -euo pipefail
sarif_path="artifacts/trivy-${{ steps.meta.outputs.release_tag }}.sarif"
if [ -f "${sarif_path}" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "::notice::Trivy SARIF report not found at ${sarif_path}; skipping SARIF upload."
fi
- name: Upload Trivy SARIF
if: always() && steps.trivy-sarif.outputs.exists == 'true'
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
sarif_file: artifacts/trivy-${{ github.ref_name }}.sarif
sarif_file: artifacts/trivy-${{ steps.meta.outputs.release_tag }}.sarif
category: ghcr-trivy
- name: Upload Trivy report artifacts
@ -340,9 +412,9 @@ jobs:
with:
name: ghcr-trivy-report
path: |
artifacts/trivy-${{ github.ref_name }}.sarif
artifacts/trivy-${{ github.ref_name }}.txt
artifacts/trivy-${{ github.ref_name }}.json
artifacts/trivy-${{ steps.meta.outputs.release_tag }}.sarif
artifacts/trivy-${{ steps.meta.outputs.release_tag }}.txt
artifacts/trivy-${{ steps.meta.outputs.release_tag }}.json
artifacts/trivy-sha-*.txt
artifacts/trivy-sha-*.json
artifacts/trivy-latest.txt

View File

@ -35,12 +35,15 @@ permissions:
contents: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
prerelease-guard:
name: Pre-release Guard
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 20
outputs:
release_tag: ${{ steps.vars.outputs.release_tag }}
@ -172,7 +175,9 @@ jobs:
build-prerelease:
name: Build Pre-release Artifact
needs: [prerelease-guard]
runs-on: [self-hosted, Linux, X64]
# Keep GNU Linux prerelease artifacts on Ubuntu 22.04 so runtime GLIBC
# symbols remain compatible with Debian 12 / Ubuntu 22.04 hosts.
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 45
steps:
- name: Checkout tag
@ -234,7 +239,7 @@ jobs:
name: Publish GitHub Pre-release
needs: [prerelease-guard, build-prerelease]
if: needs.prerelease-guard.outputs.ready_to_publish == 'true'
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 15
steps:
- name: Download prerelease artifacts

View File

@ -39,12 +39,16 @@ permissions:
id-token: write # Required for cosign keyless signing via OIDC
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
prepare:
name: Prepare Release Context
runs-on: self-hosted
if: github.event_name != 'push' || !contains(github.ref_name, '-')
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
outputs:
release_ref: ${{ steps.vars.outputs.release_ref }}
release_tag: ${{ steps.vars.outputs.release_tag }}
@ -60,7 +64,6 @@ jobs:
event_name="${GITHUB_EVENT_NAME}"
publish_release="false"
draft_release="false"
semver_pattern='^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'
if [[ "$event_name" == "push" ]]; then
release_ref="${GITHUB_REF_NAME}"
@ -87,41 +90,6 @@ jobs:
release_tag="verify-${GITHUB_SHA::12}"
fi
if [[ "$publish_release" == "true" ]]; then
if [[ ! "$release_tag" =~ $semver_pattern ]]; then
echo "::error::release_tag must match semver-like format (vX.Y.Z[-suffix])"
exit 1
fi
if ! git ls-remote --exit-code --tags "https://github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${release_tag}" >/dev/null; then
echo "::error::Tag ${release_tag} does not exist on origin. Push the tag first, then rerun manual publish."
exit 1
fi
# Guardrail: release tags must resolve to commits already reachable from main.
tmp_repo="$(mktemp -d)"
trap 'rm -rf "$tmp_repo"' EXIT
git -C "$tmp_repo" init -q
git -C "$tmp_repo" remote add origin "https://github.com/${GITHUB_REPOSITORY}.git"
git -C "$tmp_repo" fetch --quiet --filter=blob:none origin main "refs/tags/${release_tag}:refs/tags/${release_tag}"
if ! git -C "$tmp_repo" merge-base --is-ancestor "refs/tags/${release_tag}" "origin/main"; then
echo "::error::Tag ${release_tag} is not reachable from origin/main. Release tags must be cut from main."
exit 1
fi
# Guardrail: release tag and Cargo package version must stay aligned.
tag_version="${release_tag#v}"
cargo_version="$(git -C "$tmp_repo" show "refs/tags/${release_tag}:Cargo.toml" | sed -n 's/^version = "\([^"]*\)"/\1/p' | head -n1)"
if [[ -z "$cargo_version" ]]; then
echo "::error::Unable to read Cargo package version from ${release_tag}:Cargo.toml"
exit 1
fi
if [[ "$cargo_version" != "$tag_version" ]]; then
echo "::error::Tag ${release_tag} does not match Cargo.toml version (${cargo_version})."
echo "::error::Bump Cargo.toml version first, then create/publish the matching tag."
exit 1
fi
fi
{
echo "release_ref=${release_ref}"
echo "release_tag=${release_tag}"
@ -138,37 +106,143 @@ jobs:
echo "- draft_release: ${draft_release}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install gh CLI
shell: bash
run: |
set -euo pipefail
if command -v gh &>/dev/null; then
echo "gh already available: $(gh --version | head -1)"
exit 0
fi
echo "Installing gh CLI..."
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
for i in {1..60}; do
if sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; then
echo "apt/dpkg locked; waiting ($i/60)..."
sleep 5
else
break
fi
done
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 update -qq
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y gh
env:
GH_TOKEN: ${{ github.token }}
- name: Validate release trigger and authorization guard
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
python3 scripts/ci/release_trigger_guard.py \
--repo-root . \
--repository "${GITHUB_REPOSITORY}" \
--event-name "${GITHUB_EVENT_NAME}" \
--actor "${GITHUB_ACTOR}" \
--release-ref "${{ steps.vars.outputs.release_ref }}" \
--release-tag "${{ steps.vars.outputs.release_tag }}" \
--publish-release "${{ steps.vars.outputs.publish_release }}" \
--authorized-actors "${{ vars.RELEASE_AUTHORIZED_ACTORS || 'willsarg,theonlyhennygod,chumyin' }}" \
--authorized-tagger-emails "${{ vars.RELEASE_AUTHORIZED_TAGGER_EMAILS || '' }}" \
--require-annotated-tag true \
--output-json artifacts/release-trigger-guard.json \
--output-md artifacts/release-trigger-guard.md \
--fail-on-violation
env:
GH_TOKEN: ${{ github.token }}
- name: Emit release trigger audit event
if: always()
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/emit_audit_event.py \
--event-type release_trigger_guard \
--input-json artifacts/release-trigger-guard.json \
--output-json artifacts/audit-event-release-trigger-guard.json \
--artifact-name release-trigger-guard \
--retention-days 30
- name: Publish release trigger guard summary
if: always()
shell: bash
run: |
set -euo pipefail
cat artifacts/release-trigger-guard.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload release trigger guard artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: release-trigger-guard
path: |
artifacts/release-trigger-guard.json
artifacts/release-trigger-guard.md
artifacts/audit-event-release-trigger-guard.json
if-no-files-found: error
retention-days: 30
build-release:
name: Build ${{ matrix.target }}
needs: [prepare]
runs-on: ${{ matrix.os }}
timeout-minutes: 40
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ matrix.target }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ matrix.target }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/target
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
# Keep GNU Linux release artifacts on Ubuntu 22.04 to preserve
# a broadly compatible GLIBC baseline for user distributions.
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: x86_64-unknown-linux-gnu
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
- os: ubuntu-latest
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: x86_64-unknown-linux-musl
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
use_cross: true
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: aarch64-unknown-linux-gnu
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: gcc-aarch64-linux-gnu
linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER
linker: aarch64-linux-gnu-gcc
- os: ubuntu-latest
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: aarch64-unknown-linux-musl
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
use_cross: true
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: armv7-unknown-linux-gnueabihf
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: gcc-arm-linux-gnueabihf
linker_env: CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER
linker: arm-linux-gnueabihf-gcc
- os: ubuntu-latest
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: armv7-linux-androideabi
artifact: zeroclaw
archive_ext: tar.gz
@ -177,7 +251,7 @@ jobs:
linker: ""
android_ndk: true
android_api: 21
- os: ubuntu-latest
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: aarch64-linux-android
artifact: zeroclaw
archive_ext: tar.gz
@ -186,6 +260,14 @@ jobs:
linker: ""
android_ndk: true
android_api: 21
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
target: x86_64-unknown-freebsd
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
use_cross: true
- os: macos-15-intel
target: x86_64-apple-darwin
artifact: zeroclaw
@ -213,24 +295,52 @@ jobs:
with:
ref: ${{ needs.prepare.outputs.release_ref }}
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
targets: ${{ matrix.target }}
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
if: runner.os != 'Windows'
- name: Install cross for cross-built targets
if: matrix.use_cross
shell: bash
run: |
set -euo pipefail
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> "$GITHUB_PATH"
cargo install cross --locked --version 0.2.5
command -v cross
cross --version
- name: Install cross-compilation toolchain (Linux)
if: runner.os == 'Linux' && matrix.cross_compiler != ''
run: |
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y ${{ matrix.cross_compiler }}
else
apt-get update -qq
apt-get install -y ${{ matrix.cross_compiler }}
fi
set -euo pipefail
for i in {1..60}; do
if sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; then
echo "apt/dpkg locked; waiting ($i/60)..."
sleep 5
else
break
fi
done
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 update -qq
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y "${{ matrix.cross_compiler }}"
# Install matching libc dev headers for cross targets
# (required by ring/aws-lc-sys C compilation)
case "${{ matrix.target }}" in
armv7-unknown-linux-gnueabihf)
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y libc6-dev-armhf-cross ;;
aarch64-unknown-linux-gnu)
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y libc6-dev-arm64-cross ;;
esac
- name: Setup Android NDK
if: matrix.android_ndk
@ -243,13 +353,18 @@ jobs:
NDK_ROOT="${RUNNER_TEMP}/android-ndk"
NDK_HOME="${NDK_ROOT}/android-ndk-${NDK_VERSION}"
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y unzip
else
apt-get update -qq
apt-get install -y unzip
fi
for i in {1..60}; do
if sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; then
echo "apt/dpkg locked; waiting ($i/60)..."
sleep 5
else
break
fi
done
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 update -qq
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y unzip
mkdir -p "${NDK_ROOT}"
curl -fsSL "${NDK_URL}" -o "${RUNNER_TEMP}/${NDK_ZIP}"
@ -305,15 +420,25 @@ jobs:
env:
LINKER_ENV: ${{ matrix.linker_env }}
LINKER: ${{ matrix.linker }}
USE_CROSS: ${{ matrix.use_cross }}
run: |
if [ -n "$LINKER_ENV" ] && [ -n "$LINKER" ]; then
echo "Using linker override: $LINKER_ENV=$LINKER"
export "$LINKER_ENV=$LINKER"
fi
cargo build --profile release-fast --locked --target ${{ matrix.target }}
if [ "$USE_CROSS" = "true" ]; then
echo "Using cross for MUSL target"
cross build --profile release-fast --locked --target ${{ matrix.target }}
else
cargo build --profile release-fast --locked --target ${{ matrix.target }}
fi
- name: Check binary size (Unix)
if: runner.os != 'Windows'
env:
BINARY_SIZE_HARD_LIMIT_MB: 28
BINARY_SIZE_ADVISORY_MB: 20
BINARY_SIZE_TARGET_MB: 5
run: bash scripts/ci/check_binary_size.sh "target/${{ matrix.target }}/release-fast/${{ matrix.artifact }}" "${{ matrix.target }}"
- name: Package (Unix)
@ -338,47 +463,68 @@ jobs:
verify-artifacts:
name: Verify Artifact Set
needs: [prepare, build-release]
runs-on: self-hosted
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.prepare.outputs.release_ref }}
- name: Download all artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
path: artifacts
- name: Validate expected archives
- name: Validate release archive contract (verify stage)
shell: bash
run: |
set -euo pipefail
expected=(
"zeroclaw-x86_64-unknown-linux-gnu.tar.gz"
"zeroclaw-aarch64-unknown-linux-gnu.tar.gz"
"zeroclaw-armv7-unknown-linux-gnueabihf.tar.gz"
"zeroclaw-armv7-linux-androideabi.tar.gz"
"zeroclaw-aarch64-linux-android.tar.gz"
"zeroclaw-x86_64-apple-darwin.tar.gz"
"zeroclaw-aarch64-apple-darwin.tar.gz"
"zeroclaw-x86_64-pc-windows-msvc.zip"
)
python3 scripts/ci/release_artifact_guard.py \
--artifacts-dir artifacts \
--contract-file .github/release/release-artifact-contract.json \
--output-json artifacts/release-artifact-guard.verify.json \
--output-md artifacts/release-artifact-guard.verify.md \
--allow-extra-archives \
--skip-manifest-files \
--skip-sbom-files \
--skip-notice-files \
--fail-on-violation
missing=0
for file in "${expected[@]}"; do
if ! find artifacts -type f -name "$file" -print -quit | grep -q .; then
echo "::error::Missing release archive: $file"
missing=1
fi
done
- name: Emit verify-stage artifact guard audit event
if: always()
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/emit_audit_event.py \
--event-type release_artifact_guard_verify \
--input-json artifacts/release-artifact-guard.verify.json \
--output-json artifacts/audit-event-release-artifact-guard-verify.json \
--artifact-name release-artifact-guard-verify \
--retention-days 21
if [ "$missing" -ne 0 ]; then
exit 1
fi
- name: Publish verify-stage artifact guard summary
if: always()
shell: bash
run: |
set -euo pipefail
cat artifacts/release-artifact-guard.verify.md >> "$GITHUB_STEP_SUMMARY"
echo "All expected release archives are present."
- name: Upload verify-stage artifact guard reports
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: release-artifact-guard-verify
path: |
artifacts/release-artifact-guard.verify.json
artifacts/release-artifact-guard.verify.md
artifacts/audit-event-release-artifact-guard-verify.json
if-no-files-found: error
retention-days: 21
publish:
name: Publish Release
if: needs.prepare.outputs.publish_release == 'true'
needs: [prepare, verify-artifacts]
runs-on: self-hosted
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 45
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -391,8 +537,12 @@ jobs:
path: artifacts
- name: Install syft
shell: bash
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
set -euo pipefail
mkdir -p "${RUNNER_TEMP}/bin"
./scripts/ci/install_syft.sh "${RUNNER_TEMP}/bin"
echo "${RUNNER_TEMP}/bin" >> "$GITHUB_PATH"
- name: Generate SBOM (CycloneDX)
run: |
@ -409,12 +559,80 @@ jobs:
cp LICENSE-MIT artifacts/LICENSE-MIT
cp NOTICE artifacts/NOTICE
- name: Generate SHA256 checksums
- name: Generate release manifest + checksums
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
run: |
cd artifacts
find . -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.cdx.json' -o -name '*.spdx.json' -o -name 'LICENSE-APACHE' -o -name 'LICENSE-MIT' -o -name 'NOTICE' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
echo "Generated checksums:"
cat SHA256SUMS
set -euo pipefail
python3 scripts/ci/release_manifest.py \
--artifacts-dir artifacts \
--release-tag "${RELEASE_TAG}" \
--output-json artifacts/release-manifest.json \
--output-md artifacts/release-manifest.md \
--checksums-path artifacts/SHA256SUMS \
--fail-empty
- name: Generate SHA256SUMS provenance statement
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
run: |
set -euo pipefail
python3 scripts/ci/generate_provenance.py \
--artifact artifacts/SHA256SUMS \
--subject-name "zeroclaw-${RELEASE_TAG}-sha256sums" \
--output artifacts/zeroclaw.sha256sums.intoto.json
- name: Emit SHA256SUMS provenance audit event
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/emit_audit_event.py \
--event-type release_sha256sums_provenance \
--input-json artifacts/zeroclaw.sha256sums.intoto.json \
--output-json artifacts/audit-event-release-sha256sums-provenance.json \
--artifact-name release-sha256sums-provenance \
--retention-days 30
- name: Validate release artifact contract (publish stage)
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/release_artifact_guard.py \
--artifacts-dir artifacts \
--contract-file .github/release/release-artifact-contract.json \
--output-json artifacts/release-artifact-guard.publish.json \
--output-md artifacts/release-artifact-guard.publish.md \
--allow-extra-archives \
--allow-extra-manifest-files \
--allow-extra-sbom-files \
--allow-extra-notice-files \
--fail-on-violation
- name: Emit publish-stage artifact guard audit event
if: always()
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/emit_audit_event.py \
--event-type release_artifact_guard_publish \
--input-json artifacts/release-artifact-guard.publish.json \
--output-json artifacts/audit-event-release-artifact-guard-publish.json \
--artifact-name release-artifact-guard-publish \
--retention-days 30
- name: Publish artifact guard summary
shell: bash
run: |
set -euo pipefail
cat artifacts/release-artifact-guard.publish.md >> "$GITHUB_STEP_SUMMARY"
- name: Publish release manifest summary
shell: bash
run: |
set -euo pipefail
cat artifacts/release-manifest.md >> "$GITHUB_STEP_SUMMARY"
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
@ -431,6 +649,26 @@ jobs:
"$file"
done < <(find artifacts -type f ! -name '*.sig' ! -name '*.pem' ! -name '*.sigstore.json' -print0)
- name: Compose release-notes supply-chain references
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
run: |
set -euo pipefail
python3 scripts/ci/release_notes_with_supply_chain_refs.py \
--artifacts-dir artifacts \
--repository "${GITHUB_REPOSITORY}" \
--release-tag "${RELEASE_TAG}" \
--output-json artifacts/release-notes-supply-chain.json \
--output-md artifacts/release-notes-supply-chain.md \
--fail-on-missing
- name: Publish release-notes supply-chain summary
shell: bash
run: |
set -euo pipefail
cat artifacts/release-notes-supply-chain.md >> "$GITHUB_STEP_SUMMARY"
- name: Verify GHCR release tag availability
shell: bash
env:
@ -476,6 +714,7 @@ jobs:
with:
tag_name: ${{ needs.prepare.outputs.release_tag }}
draft: ${{ needs.prepare.outputs.draft_release == 'true' }}
body_path: artifacts/release-notes-supply-chain.md
generate_release_notes: true
files: |
artifacts/**/*

103
.github/workflows/release-build.yml vendored Normal file
View File

@ -0,0 +1,103 @@
name: Production Release Build
on:
push:
branches: ["main"]
tags: ["v*"]
workflow_dispatch:
concurrency:
group: production-release-build-${{ github.ref || github.run_id }}
cancel-in-progress: false
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
ENSURE_RUST_COMPONENTS: "rustfmt clippy"
jobs:
build-and-test:
name: Build and Test (Linux x86_64)
runs-on: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
components: rustfmt, clippy
- name: Ensure C toolchain for Rust builds
shell: bash
run: ./scripts/ci/ensure_cc.sh
- name: Ensure rustfmt and clippy components
shell: bash
run: rustup component add rustfmt clippy --toolchain 1.92.0
- name: Activate toolchain binaries on PATH
shell: bash
run: |
set -euo pipefail
toolchain_bin="$(dirname "$(rustup which --toolchain 1.92.0 cargo)")"
echo "$toolchain_bin" >> "$GITHUB_PATH"
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- name: Cache Cargo registry and target
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: production-release-build
shared-key: ${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
cache-targets: true
cache-bin: false
- name: Rust quality gates
shell: bash
run: |
set -euo pipefail
./scripts/ci/rust_quality_gate.sh
cargo test --locked --lib --bins --verbose
- name: Build production binary (canonical)
shell: bash
run: cargo build --release --locked
- name: Prepare artifact bundle
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
cp target/release/zeroclaw artifacts/zeroclaw
sha256sum artifacts/zeroclaw > artifacts/zeroclaw.sha256
- name: Upload production artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: zeroclaw-linux-amd64
path: |
artifacts/zeroclaw
artifacts/zeroclaw.sha256
if-no-files-found: error
retention-days: 21

View File

@ -0,0 +1,61 @@
// Enforce at least one human approval on pull requests.
// Used by .github/workflows/ci-run.yml via actions/github-script.
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request?.number;
if (!prNumber) {
core.setFailed("Missing pull_request context.");
return;
}
const botAllowlist = new Set(
(process.env.HUMAN_REVIEW_BOT_LOGINS || "github-actions[bot],dependabot[bot],coderabbitai[bot]")
.split(",")
.map((value) => value.trim().toLowerCase())
.filter(Boolean),
);
const isBotAccount = (login, accountType) => {
if (!login) return false;
if ((accountType || "").toLowerCase() === "bot") return true;
if (login.endsWith("[bot]")) return true;
return botAllowlist.has(login);
};
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const latestReviewByUser = new Map();
const decisiveStates = new Set(["APPROVED", "CHANGES_REQUESTED", "DISMISSED"]);
for (const review of reviews) {
const login = review.user?.login?.toLowerCase();
if (!login) continue;
if (!decisiveStates.has(review.state)) continue;
latestReviewByUser.set(login, {
state: review.state,
type: review.user?.type || "",
});
}
const humanApprovers = [];
for (const [login, review] of latestReviewByUser.entries()) {
if (review.state !== "APPROVED") continue;
if (isBotAccount(login, review.type)) continue;
humanApprovers.push(login);
}
if (humanApprovers.length === 0) {
core.setFailed(
"No human approving review found. At least one non-bot approval is required before merge.",
);
return;
}
core.info(`Human approval check passed. Approver(s): ${humanApprovers.join(", ")}`);
};

View File

@ -1,83 +0,0 @@
// Extracted from ci-run.yml step: Require owner approval for workflow file changes
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request?.number;
const prAuthor = context.payload.pull_request?.user?.login?.toLowerCase() || "";
if (!prNumber) {
core.setFailed("Missing pull_request context.");
return;
}
const baseOwners = ["theonlyhennygod", "willsarg", "chumyin"];
const configuredOwners = (process.env.WORKFLOW_OWNER_LOGINS || "")
.split(",")
.map((login) => login.trim().toLowerCase())
.filter(Boolean);
const ownerAllowlist = [...new Set([...baseOwners, ...configuredOwners])];
if (ownerAllowlist.length === 0) {
core.setFailed("Workflow owner allowlist is empty.");
return;
}
core.info(`Workflow owner allowlist: ${ownerAllowlist.join(", ")}`);
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const workflowFiles = files
.map((file) => file.filename)
.filter((name) => name.startsWith(".github/workflows/"));
if (workflowFiles.length === 0) {
core.info("No workflow files changed in this PR.");
return;
}
core.info(`Workflow files changed:\n- ${workflowFiles.join("\n- ")}`);
if (prAuthor && ownerAllowlist.includes(prAuthor)) {
core.info(`Workflow PR authored by allowlisted owner: @${prAuthor}`);
return;
}
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const latestReviewByUser = new Map();
for (const review of reviews) {
const login = review.user?.login;
if (!login) continue;
latestReviewByUser.set(login.toLowerCase(), review.state);
}
const approvedUsers = [...latestReviewByUser.entries()]
.filter(([, state]) => state === "APPROVED")
.map(([login]) => login);
if (approvedUsers.length === 0) {
core.setFailed("Workflow files changed but no approving review is present.");
return;
}
const ownerApprover = approvedUsers.find((login) => ownerAllowlist.includes(login));
if (!ownerApprover) {
core.setFailed(
`Workflow files changed. Approvals found (${approvedUsers.join(", ")}), but none match workflow owner allowlist.`,
);
return;
}
core.info(`Workflow owner approval present: @${ownerApprover}`);
};

View File

@ -4,8 +4,8 @@
// Required environment variables:
// RUST_CHANGED — "true" if Rust files changed
// DOCS_CHANGED — "true" if docs files changed
// LINT_RESULT — result of the lint job
// LINT_DELTA_RESULT — result of the strict delta lint job
// LINT_RESULT — result of the quality-gate job (fmt + clippy)
// LINT_DELTA_RESULT — result of the quality-gate job (strict delta)
// DOCS_RESULT — result of the docs-quality job
module.exports = async ({ github, context, core }) => {
@ -23,10 +23,10 @@ module.exports = async ({ github, context, core }) => {
const failures = [];
if (rustChanged && !["success", "skipped"].includes(lintResult)) {
failures.push("`Lint Gate (Format + Clippy)` failed.");
failures.push("`Quality Gate (Format + Clippy)` failed.");
}
if (rustChanged && !["success", "skipped"].includes(lintDeltaResult)) {
failures.push("`Lint Gate (Strict Delta)` failed.");
failures.push("`Quality Gate (Strict Delta)` failed.");
}
if (docsChanged && !["success", "skipped"].includes(docsResult)) {
failures.push("`Docs Quality` failed.");

View File

@ -83,6 +83,7 @@ module.exports = async ({ github, context, core }) => {
if (dangerousProblems.length > 0) {
blockingFindings.push(`Dangerous patch markers found (${dangerousProblems.length})`);
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
@ -124,13 +125,11 @@ module.exports = async ({ github, context, core }) => {
const isBlocking = blockingFindings.length > 0;
const ownerApprovalNote = workflowFilesChanged.length > 0
const workflowChangeNote = workflowFilesChanged.length > 0
? [
"",
"Workflow files changed in this PR:",
...workflowFilesChanged.map((name) => `- \`${name}\``),
"",
"Reminder: workflow changes require owner approval via `CI Required Gate`.",
].join("\n")
: "";
@ -149,11 +148,12 @@ module.exports = async ({ github, context, core }) => {
"Action items:",
"1. Complete required PR template sections/fields.",
"2. Remove tabs, trailing whitespace, and merge conflict markers from added lines.",
"3. Re-run local checks before pushing:",
"4. Re-run local checks before pushing:",
" - `./scripts/ci/rust_quality_gate.sh`",
" - `./scripts/ci/rust_strict_delta_gate.sh`",
" - `./scripts/ci/docs_quality_gate.sh`",
"",
"",
`Run logs: ${runUrl}`,
"",
"Detected blocking line issues (sample):",
@ -161,7 +161,7 @@ module.exports = async ({ github, context, core }) => {
"",
"Detected advisory line issues (sample):",
...(advisoryDetails.length > 0 ? advisoryDetails : ["- none"]),
ownerApprovalNote,
workflowChangeNote,
].join("\n");
if (existing) {

View File

@ -15,6 +15,9 @@ on:
- ".github/security/unsafe-audit-governance.json"
- "scripts/ci/install_gitleaks.sh"
- "scripts/ci/install_syft.sh"
- "scripts/ci/ensure_c_toolchain.sh"
- "scripts/ci/ensure_cargo_component.sh"
- "scripts/ci/self_heal_rust_toolchain.sh"
- "scripts/ci/deny_policy_guard.py"
- "scripts/ci/secrets_governance_guard.py"
- "scripts/ci/unsafe_debt_audit.py"
@ -22,29 +25,12 @@ on:
- "scripts/ci/config/unsafe_debt_policy.toml"
- "scripts/ci/emit_audit_event.py"
- "scripts/ci/security_regression_tests.sh"
- "scripts/ci/ensure_cc.sh"
- ".github/workflows/sec-audit.yml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "deny.toml"
- ".gitleaks.toml"
- ".github/security/gitleaks-allowlist-governance.json"
- ".github/security/deny-ignore-governance.json"
- ".github/security/unsafe-audit-governance.json"
- "scripts/ci/install_gitleaks.sh"
- "scripts/ci/install_syft.sh"
- "scripts/ci/deny_policy_guard.py"
- "scripts/ci/secrets_governance_guard.py"
- "scripts/ci/unsafe_debt_audit.py"
- "scripts/ci/unsafe_policy_guard.py"
- "scripts/ci/config/unsafe_debt_policy.toml"
- "scripts/ci/emit_audit_event.py"
- "scripts/ci/security_regression_tests.sh"
- ".github/workflows/sec-audit.yml"
# Do not gate pull_request by paths: main branch protection requires
# "Security Required Gate" to always report a status on PRs.
merge_group:
branches: [dev, main]
schedule:
@ -78,68 +64,81 @@ permissions:
checks: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
security-scope:
name: Security Scope
runs-on: [self-hosted, Linux, X64]
# --- Change detection for fast-path on non-Rust PRs ---
changes:
name: Detect Change Scope
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
outputs:
run_heavy: ${{ steps.detect.outputs.run_heavy }}
steps:
- name: Detect heavy security scope
id: detect
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
if (context.eventName !== "pull_request") {
core.setOutput("run_heavy", "true");
return;
}
const files = await github.paginate(
github.rest.pulls.listFiles,
{
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
per_page: 100,
},
);
const isRustSurface = (path) =>
path === "Cargo.toml" ||
path === "Cargo.lock" ||
path.startsWith("src/") ||
path.startsWith("crates/") ||
path.startsWith("tests/");
const runHeavy = files.some((file) => isRustSurface(file.filename));
core.info(`Heavy security jobs enabled: ${runHeavy}`);
core.setOutput("run_heavy", runHeavy ? "true" : "false");
audit:
name: Security Audit
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 20
rust_changed: ${{ steps.scope.outputs.rust_changed }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Detect Rust changes
id: scope
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event.before }}
run: ./scripts/ci/detect_change_scope.sh
# --- Consolidated Rust security: audit + deny + security regressions ---
# Merges 3 separate Rust-compiling jobs into 1 sequential job on 8 vCPU.
rust-security:
name: Rust Security (Audit + Deny + Regressions)
needs: [changes]
if: >-
needs.changes.outputs.rust_changed == 'true' ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 25
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: zeroclaw-ci-v1
shared-key: ${{ runner.os }}-rust
cache-targets: true
cache-bin: false
# --- Step 1: cargo-audit (was: audit job) ---
- uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
deny:
name: License & Supply Chain
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# --- Step 2: cargo-deny (was: deny job) ---
- name: Enforce deny policy hygiene
shell: bash
run: |
@ -152,9 +151,46 @@ jobs:
--output-md artifacts/deny-policy-guard.md \
--fail-on-violation
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
with:
command: check advisories licenses sources
- name: Install cargo-deny
shell: bash
run: |
set -euo pipefail
version="0.19.0"
arch="$(uname -m)"
case "${arch}" in
x86_64|amd64)
target="x86_64-unknown-linux-musl"
expected_sha256="0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f"
;;
aarch64|arm64)
target="aarch64-unknown-linux-musl"
expected_sha256="2b3567a60b7491c159d1cef8b7d8479d1ad2a31e29ef49462634ad4552fcc77d"
;;
*)
echo "Unsupported runner architecture for cargo-deny: ${arch}" >&2
exit 1
;;
esac
install_dir="${RUNNER_TEMP}/cargo-deny-${version}"
archive="${RUNNER_TEMP}/cargo-deny-${version}-${target}.tar.gz"
mkdir -p "${install_dir}"
curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \
--output "${archive}" \
"https://github.com/EmbarkStudios/cargo-deny/releases/download/${version}/cargo-deny-${version}-${target}.tar.gz"
actual_sha256="$(sha256sum "${archive}" | awk '{print $1}')"
if [ "${actual_sha256}" != "${expected_sha256}" ]; then
echo "Checksum mismatch for cargo-deny ${version} (${target})" >&2
echo "Expected: ${expected_sha256}" >&2
echo "Actual: ${actual_sha256}" >&2
exit 1
fi
tar -xzf "${archive}" -C "${install_dir}" --strip-components=1
echo "${install_dir}" >> "${GITHUB_PATH}"
"${install_dir}/cargo-deny" --version
- name: Run cargo-deny checks
shell: bash
run: cargo-deny check advisories licenses sources
- name: Emit deny audit event
if: always()
@ -188,27 +224,27 @@ jobs:
if-no-files-found: ignore
retention-days: 14
security-regressions:
name: Security Regression Tests
needs: [security-scope]
if: needs.security-scope.outputs.run_heavy == 'true'
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: sec-audit-security-regressions
# --- Step 3: Security regression tests (was: security-regressions job) ---
- name: Run security regression suite
shell: bash
run: ./scripts/ci/security_regression_tests.sh
# --- Fast-path for non-Rust PRs (no compilation needed) ---
rust-security-skipped:
name: Rust Security (Skipped — Non-Rust PR)
needs: [changes]
if: >-
needs.changes.outputs.rust_changed != 'true' &&
github.event_name != 'schedule' &&
github.event_name != 'workflow_dispatch'
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Skip Rust security for non-Rust PR
run: echo "Non-Rust PR; Rust security checks skipped."
secrets:
name: Secrets Governance (Gitleaks)
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -401,13 +437,15 @@ jobs:
if-no-files-found: ignore
retention-days: 14
sbom:
name: SBOM Snapshot
runs-on: [self-hosted, Linux, X64]
# --- Compliance: SBOM + unsafe debt (no Rust compilation needed) ---
compliance:
name: Compliance (SBOM + Unsafe Debt)
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# --- SBOM (was: sbom job) ---
- name: Install syft
shell: bash
run: |
@ -466,15 +504,7 @@ jobs:
if-no-files-found: ignore
retention-days: 14
unsafe-debt:
name: Unsafe Debt Audit
needs: [security-scope]
if: needs.security-scope.outputs.run_heavy == 'true'
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# --- Unsafe debt (was: unsafe-debt job) ---
- name: Enforce unsafe policy governance
shell: bash
run: |
@ -608,38 +638,40 @@ jobs:
security-required:
name: Security Required Gate
if: always() && (github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'merge_group')
needs: [audit, deny, security-regressions, secrets, sbom, unsafe-debt]
runs-on: [self-hosted, Linux, X64]
needs: [changes, rust-security, rust-security-skipped, secrets, compliance]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Enforce security gate
shell: bash
run: |
set -euo pipefail
results=(
"audit=${{ needs.audit.result }}"
"deny=${{ needs.deny.result }}"
"security-regressions=${{ needs.security-regressions.result }}"
"secrets=${{ needs.secrets.result }}"
"sbom=${{ needs.sbom.result }}"
"unsafe-debt=${{ needs['unsafe-debt'].result }}"
)
for item in "${results[@]}"; do
echo "$item"
done
for item in "${results[@]}"; do
key="${item%%=*}"
result="${item#*=}"
rust_changed="${{ needs.changes.outputs.rust_changed }}"
if [ "$key" = "security-regressions" ] || [ "$key" = "unsafe-debt" ]; then
if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then
echo "Security gate failed: $item"
exit 1
fi
continue
fi
if [ "$result" != "success" ]; then
echo "Security gate failed: $item"
# Rust security: must pass if Rust changed, or must be skipped
if [ "$rust_changed" = "true" ]; then
rust_sec="${{ needs.rust-security.result }}"
if [ "$rust_sec" != "success" ]; then
echo "Security gate failed: rust-security=${rust_sec}"
exit 1
fi
done
else
rust_skip="${{ needs.rust-security-skipped.result }}"
if [ "$rust_skip" != "success" ]; then
echo "Security gate failed: rust-security-skipped=${rust_skip}"
exit 1
fi
fi
# Non-Rust security: always required
secrets_result="${{ needs.secrets.result }}"
compliance_result="${{ needs.compliance.result }}"
echo "secrets=${secrets_result}"
echo "compliance=${compliance_result}"
if [ "$secrets_result" != "success" ] || [ "$compliance_result" != "success" ]; then
echo "Security gate failed: secrets=${secrets_result} compliance=${compliance_result}"
exit 1
fi
echo "All security checks passed."

View File

@ -1,5 +1,7 @@
name: Sec CodeQL
# Moved off PR path per CI/CD optimization PRD.
# Runs on push-to-main/dev + weekly schedule to catch vulnerabilities within 1 merge.
on:
push:
branches: [dev, main]
@ -8,25 +10,18 @@ on:
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/ensure_c_toolchain.sh"
- "scripts/ci/ensure_cargo_component.sh"
- ".github/codeql/**"
- "scripts/ci/self_heal_rust_toolchain.sh"
- "scripts/ci/ensure_cc.sh"
- ".github/workflows/sec-codeql.yml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- ".github/codeql/**"
- ".github/workflows/sec-codeql.yml"
merge_group:
branches: [dev, main]
schedule:
- cron: "0 6 * * 1" # Weekly Monday 6am UTC
workflow_dispatch:
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref || github.run_id }}
group: codeql-${{ github.ref || github.run_id }}
cancel-in-progress: true
permissions:
@ -34,17 +29,30 @@ permissions:
security-events: write
actions: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
codeql:
name: CodeQL Analysis
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 30
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 45
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
@ -53,19 +61,26 @@ jobs:
queries: security-and-quality
- name: Set up Rust
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure native build tools
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- name: Ensure cargo component
shell: bash
run: |
SUDO=""
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
$SUDO apt-get update
$SUDO apt-get install -y --no-install-recommends build-essential pkg-config
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: zeroclaw-ci-v1
shared-key: ${{ runner.os }}-rust
cache-targets: true
cache-bin: false
- name: Build
run: cargo build --workspace --all-targets --locked
@ -74,3 +89,13 @@ jobs:
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
category: "/language:rust"
- name: Summarize runner
if: always()
shell: bash
run: |
{
echo "### CodeQL Runner"
echo "- Branch: \`${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}\`"
echo "- Runner: \`blacksmith-8vcpu-ubuntu-2404\`"
} >> "$GITHUB_STEP_SUMMARY"

View File

@ -82,10 +82,16 @@ permissions:
checks: write
pull-requests: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
vorpal:
name: Vorpal Reviewdog Scan
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 20
steps:
- name: Checkout

View File

@ -17,7 +17,8 @@ permissions:
jobs:
update-notice:
name: Update NOTICE with new contributors
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

View File

@ -14,12 +14,15 @@ permissions:
pull-requests: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
benchmarks:
name: Criterion Benchmarks
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

View File

@ -6,32 +6,68 @@ on:
paths:
- "Cargo.toml"
- "Cargo.lock"
- "deny.toml"
- "src/**"
- "crates/**"
- "tests/**"
- "scripts/**"
- "scripts/ci/ensure_cc.sh"
- ".github/workflows/test-e2e.yml"
workflow_dispatch:
concurrency:
group: e2e-${{ github.event.pull_request.number || github.sha }}
group: test-e2e-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
integration-tests:
name: Integration / E2E Tests
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 30
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 20
env:
ENSURE_RUST_COMPONENTS: ""
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
toolchain: 1.92.0
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: zeroclaw-ci-v1
shared-key: ${{ runner.os }}-rust
cache-targets: true
cache-bin: false
- name: Runner preflight (compiler + disk)
shell: bash
run: |
set -euo pipefail
echo "Runner: ${RUNNER_NAME:-unknown} (${RUNNER_OS:-unknown}/${RUNNER_ARCH:-unknown})"
if ! command -v cc >/dev/null 2>&1; then
echo "::error::Missing 'cc' compiler on runner. Install build-essential (Debian/Ubuntu) or equivalent."
exit 1
fi
cc --version | head -n1
free_kb="$(df -Pk . | awk 'NR==2 {print $4}')"
min_kb=$((10 * 1024 * 1024))
if [ "${free_kb}" -lt "${min_kb}" ]; then
echo "::error::Insufficient disk space on runner (<10 GiB free)."
df -h .
exit 1
fi
- name: Run integration / E2E tests
run: cargo test --test agent_e2e --locked --verbose

View File

@ -19,12 +19,15 @@ permissions:
issues: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
fuzz:
name: Fuzz (${{ matrix.target }})
runs-on: [self-hosted, Linux, X64]
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
strategy:
fail-fast: false

View File

@ -1,62 +0,0 @@
name: Test Rust Build
on:
workflow_call:
inputs:
run_command:
description: "Shell command(s) to execute."
required: true
type: string
timeout_minutes:
description: "Job timeout in minutes."
required: false
default: 20
type: number
toolchain:
description: "Rust toolchain channel/version."
required: false
default: "stable"
type: string
components:
description: "Optional rustup components."
required: false
default: ""
type: string
targets:
description: "Optional rustup targets."
required: false
default: ""
type: string
use_cache:
description: "Whether to enable rust-cache."
required: false
default: true
type: boolean
permissions:
contents: read
jobs:
run:
runs-on: [self-hosted, Linux, X64]
timeout-minutes: ${{ inputs.timeout_minutes }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: ${{ inputs.toolchain }}
components: ${{ inputs.components }}
targets: ${{ inputs.targets }}
- name: Restore Rust cache
if: inputs.use_cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
- name: Run command
shell: bash
run: |
set -euo pipefail
${{ inputs.run_command }}

90
.github/workflows/test-self-hosted.yml vendored Normal file
View File

@ -0,0 +1,90 @@
name: Test Self-Hosted Runner
on:
workflow_dispatch:
schedule:
- cron: "30 2 * * *"
permissions:
contents: read
jobs:
runner-health:
name: Runner Health / self-hosted aws-india
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404]
timeout-minutes: 10
steps:
- name: Check runner info
run: |
echo "Runner: $(hostname)"
echo "OS: $(uname -a)"
echo "User: $(whoami)"
if command -v rustc >/dev/null 2>&1; then
echo "Rust: $(rustc --version)"
else
echo "Rust: <not installed>"
fi
if command -v cargo >/dev/null 2>&1; then
echo "Cargo: $(cargo --version)"
else
echo "Cargo: <not installed>"
fi
if command -v cc >/dev/null 2>&1; then
echo "CC: $(cc --version | head -n1)"
else
echo "CC: <not installed>"
fi
if command -v gcc >/dev/null 2>&1; then
echo "GCC: $(gcc --version | head -n1)"
else
echo "GCC: <not installed>"
fi
if command -v clang >/dev/null 2>&1; then
echo "Clang: $(clang --version | head -n1)"
else
echo "Clang: <not installed>"
fi
if command -v docker >/dev/null 2>&1; then
echo "Docker: $(docker --version)"
else
echo "Docker: <not installed>"
fi
- name: Verify compiler + disk prerequisites
shell: bash
run: |
set -euo pipefail
failed=0
if ! command -v cc >/dev/null 2>&1; then
echo "::error::Missing 'cc'. Install build-essential (or gcc/clang + symlink)."
failed=1
fi
free_kb="$(df -Pk . | awk 'NR==2 {print $4}')"
min_kb=$((10 * 1024 * 1024))
if [ "${free_kb}" -lt "${min_kb}" ]; then
echo "::error::Disk free below 10 GiB; clean runner workspace/cache."
df -h .
failed=1
fi
inode_used_pct="$(df -Pi . | awk 'NR==2 {gsub(/%/, "", $5); print $5}')"
if [ "${inode_used_pct}" -ge 95 ]; then
echo "::error::Inode usage >=95%; clean files to avoid ENOSPC."
df -i .
failed=1
fi
if [ "${failed}" -ne 0 ]; then
exit 1
fi
- name: Test Docker
shell: bash
run: |
set -euo pipefail
if ! command -v docker >/dev/null 2>&1; then
echo "::notice::Docker is not installed on this self-hosted runner. Skipping docker smoke test."
exit 0
fi
docker run --rm hello-world

View File

@ -7,6 +7,7 @@ on:
- ".github/*.yml"
- ".github/*.yaml"
push:
branches: [dev, main]
paths:
- ".github/workflows/**"
- ".github/*.yml"
@ -19,11 +20,23 @@ concurrency:
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
no-tabs:
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 10
steps:
- name: Normalize git global hooks config
shell: bash
run: |
set -euo pipefail
git config --global --unset-all core.hooksPath || true
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -54,11 +67,41 @@ jobs:
PY
actionlint:
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 10
steps:
- name: Normalize git global hooks config
shell: bash
run: |
set -euo pipefail
git config --global --unset-all core.hooksPath || true
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install actionlint binary
shell: bash
run: |
set -euo pipefail
version="1.7.11"
arch="$(uname -m)"
case "$arch" in
x86_64|amd64) archive="actionlint_${version}_linux_amd64.tar.gz" ;;
aarch64|arm64) archive="actionlint_${version}_linux_arm64.tar.gz" ;;
*)
echo "::error::Unsupported architecture: ${arch}"
exit 1
;;
esac
curl -fsSL \
-o "$RUNNER_TEMP/actionlint.tgz" \
"https://github.com/rhysd/actionlint/releases/download/v${version}/${archive}"
tar -xzf "$RUNNER_TEMP/actionlint.tgz" -C "$RUNNER_TEMP" actionlint
chmod +x "$RUNNER_TEMP/actionlint"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
"$RUNNER_TEMP/actionlint" -version
- name: Lint GitHub workflows
uses: rhysd/actionlint@393031adb9afb225ee52ae2ccd7a5af5525e03e8 # v1.7.11
shell: bash
run: actionlint -color

11
.gitignore vendored
View File

@ -1,4 +1,6 @@
/target
/target_ci
/target_review*
firmware/*/target
*.db
*.db-journal
@ -13,6 +15,9 @@ site/.vite/
site/public/docs-content/
gh-pages/
.idea
.claude
# Environment files (may contain secrets)
.env
@ -29,8 +34,12 @@ venv/
# Secret keys and credentials
.secret_key
otp-secret
*.key
*.pem
credentials.json
/config.toml
.worktrees/
ZEROCLAW_CONTEXT.md
# Nix
result

View File

@ -3,6 +3,22 @@
This file defines the default working protocol for coding agents in this repository.
Scope: entire repository.
## 0) Session Default Target (Mandatory)
- When operator intent does not explicitly specify another repository/path, treat the active coding target as this repository (`/home/ubuntu/zeroclaw`).
- Do not switch to or implement in other repositories unless the operator explicitly requests that scope in the current conversation.
- Ambiguous wording (for example "这个仓库", "当前项目", "the repo") is resolved to `/home/ubuntu/zeroclaw` by default.
- Context mentioning external repositories does not authorize cross-repo edits; explicit current-turn override is required.
- Before any repo-affecting action, verify target lock (`pwd` + git root) to prevent accidental execution in sibling repositories.
## 0.1) Clean Worktree First Gate (Mandatory)
- Before handling any repository content (analysis, debugging, coding, tests, docs, CI), create a **new clean dedicated git worktree** for the active task.
- Do not perform substantive task work in a dirty workspace.
- Do not reuse a previously dirty worktree for a new task track.
- If the current location is dirty, stop and bootstrap a clean worktree/branch first.
- If worktree bootstrap fails, stop and report the blocker; do not continue in-place.
## 1) Project Snapshot (Read First)
ZeroClaw is a Rust-first autonomous agent runtime optimized for:
@ -240,8 +256,8 @@ All contributors (human or agent) must follow the same collaboration flow:
- Create and work from a non-`main` branch.
- Commit changes to that branch with clear, scoped commit messages.
- Open a PR to `main`; do not push directly to `main`.
- `main` is the integration branch for reviewed changes.
- Open a PR to `main` by default (`dev` is optional for integration batching); do not push directly to `dev` or `main`.
- `main` accepts direct PR merges after required checks and review policy pass.
- Wait for required checks and review outcomes before merging.
- Merge via PR controls (squash/rebase/merge as repository policy allows).
- After merge/close, clean up task branches/worktrees that are no longer needed.
@ -251,7 +267,7 @@ All contributors (human or agent) must follow the same collaboration flow:
- Decide merge/close outcomes from repository-local authority in this order: `.github/workflows/**`, GitHub branch protection/rulesets, `docs/pr-workflow.md`, then this `AGENTS.md`.
- External agent skills/templates are execution aids only; they must not override repository-local policy.
- A normal contributor PR targeting `main` is expected; evaluate by intent, scope, and policy compliance.
- A normal contributor PR targeting `main` is valid under the main-first flow when required checks and review policy are satisfied; use `dev` only for explicit integration batching.
- Direct-close the PR (do not supersede/replay) when high-confidence integrity-risk signals exist:
- unapproved or unrelated repository rebranding attempts (for example replacing project logo/identity assets)
- unauthorized platform-surface expansion (for example introducing `web` apps, dashboards, frontend stacks, or UI surfaces not requested by maintainers)
@ -350,7 +366,6 @@ Use these rules to keep the trait/factory architecture stable under growth.
- Apply `docs/i18n-guide.md` completion checklist before merge and include i18n status in PR notes.
- For docs snapshots, add new date-stamped files for new sprints rather than rewriting historical context.
## 8) Validation Matrix
Default local checks for code changes:

View File

@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
value if the input used the legacy `enc:` format
- `SecretStore::needs_migration()` — Check if a value uses the legacy `enc:` format
- `SecretStore::is_secure_encrypted()` — Check if a value uses the secure `enc2:` format
- `feishu_doc` tool — Feishu/Lark document operations (`read`, `write`, `append`, `create`, `list_blocks`, `get_block`, `update_block`, `delete_block`, `create_table`, `write_table_cells`, `create_table_with_values`, `upload_image`, `upload_file`)
- Agent session persistence guidance now includes explicit backend/strategy/TTL key names for rollout notes.
- **Telegram mention_only mode** — New config option `mention_only` for Telegram channel.
When enabled, bot only responds to messages that @-mention the bot in group chats.
Direct messages always work regardless of this setting. Default: `false`.
@ -27,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Legacy values are still decrypted for backward compatibility but should be migrated.
### Fixed
- **Gemini thinking model support** — Responses from thinking models (e.g. `gemini-3-pro-preview`)
are now handled correctly. The provider skips internal reasoning parts (`thought: true`) and
signature parts (`thoughtSignature`), extracting only the final answer text. Falls back to
@ -64,4 +67,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Workspace escape prevention
- Forbidden system path protection (`/etc`, `/root`, `~/.ssh`)
[0.1.0]: https://github.com/theonlyhennygod/zeroclaw/releases/tag/v0.1.0
[0.1.0]: https://github.com/zeroclaw-labs/zeroclaw/releases/tag/v0.1.0

111
CLAUDE.md
View File

@ -1,90 +1,31 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Quick Reference — Build, Test, Lint
```bash
# Build (debug)
cargo build
# Build (release, optimized for size)
cargo build --release
# Lint
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
# Test (all)
cargo test
# Test (single test by name)
cargo test test_name_substring
# Test (single integration test file)
cargo test --test agent_e2e
# Benchmarks
cargo bench
# Full local CI in Docker (recommended before PR)
./dev/ci.sh all
```
Rust edition: 2021. MSRV: 1.87. Binary name: `zeroclaw`. Unsafe code is forbidden (`#![forbid(unsafe_code)]`).
## Workspace Structure
Cargo workspace with two members:
- `.` (root) — the main `zeroclaw` binary crate
- `crates/robot-kit``zeroclaw-robot-kit`, a standalone robotics toolkit (drive, vision, speech, sensors, safety)
## Feature Flags
Default features: `channel-lark`, `web-fetch-html2md`. Notable opt-in features:
- `hardware` — USB/serial peripheral support (nusb + tokio-serial)
- `channel-matrix` — Matrix/Element E2EE channel
- `memory-postgres` — PostgreSQL memory backend
- `observability-otel` — OpenTelemetry OTLP traces/metrics
- `browser-native` — Rust-native browser automation (fantoccini/WebDriver)
- `runtime-wasm` — In-process WASM sandbox (wasmi)
- `sandbox-landlock` / `sandbox-bubblewrap` — Linux kernel sandboxing
- `peripheral-rpi` — Raspberry Pi GPIO (rppal, Linux only)
- `whatsapp-web` — Native WhatsApp Web client (wa-rs)
- `probe` — probe-rs for STM32/Nucleo debug probe
- `rag-pdf` — PDF extraction for datasheet RAG
## Architecture Overview
ZeroClaw is a trait-driven, modular autonomous agent runtime. The core pattern: define a trait in `<subsystem>/traits.rs`, implement it in sibling modules, register implementations in a factory function in `<subsystem>/mod.rs`.
Key extension points (traits):
- `src/providers/traits.rs``Provider` (model inference backends)
- `src/channels/traits.rs``Channel` (messaging platform integrations)
- `src/tools/traits.rs``Tool` (agent-callable capabilities)
- `src/memory/traits.rs``Memory` (persistence backends)
- `src/observability/traits.rs``Observer` (telemetry/metrics)
- `src/runtime/traits.rs``RuntimeAdapter` (execution environments)
- `src/peripherals/traits.rs``Peripheral` (hardware boards)
**Data flow**: User message arrives via a `Channel` -> `agent/loop_.rs` orchestrates the conversation -> `Provider` generates LLM responses -> `Tool` executions are dispatched -> results flow back through the channel. `SecurityPolicy` (`src/security/policy.rs`) enforces access control across all tool executions. `Config` (`src/config/schema.rs`) is the single source for all runtime configuration and is effectively a public API.
**Provider resilience**: `ReliableProvider` (`src/providers/reliable.rs`) wraps providers with fallback chains and automatic retry. `router.rs` handles model routing across multiple providers.
**Gateway**: `src/gateway/` is an axum-based HTTP server with webhook endpoints, SSE streaming, WebSocket support, and an OpenAI-compatible API layer.
## Engineering Protocol
# CLAUDE.md — ZeroClaw Agent Engineering Protocol
This file defines the default working protocol for Claude agents in this repository.
Scope: entire repository.
## 1) Project Snapshot (Read First)
ZeroClaw is a Rust-first autonomous agent runtime optimized for high performance, efficiency, stability, extensibility, sustainability, and security.
ZeroClaw is a Rust-first autonomous agent runtime optimized for:
- high performance
- high efficiency
- high stability
- high extensibility
- high sustainability
- high security
Core architecture is trait-driven and modular. Most extension work should be done by implementing traits and registering in factory modules.
Key extension points:
- `src/providers/traits.rs` (`Provider`)
- `src/channels/traits.rs` (`Channel`)
- `src/tools/traits.rs` (`Tool`)
- `src/memory/traits.rs` (`Memory`)
- `src/observability/traits.rs` (`Observer`)
- `src/runtime/traits.rs` (`RuntimeAdapter`)
- `src/peripherals/traits.rs` (`Peripheral`) — hardware boards (STM32, RPi GPIO)
## 2) Deep Architecture Observations (Why This Protocol Exists)
These codebase realities should drive every design decision:
@ -202,13 +143,8 @@ Required:
- `src/channels/` — Telegram/Discord/Slack/etc channels
- `src/tools/` — tool execution surface (shell, file, memory, browser)
- `src/peripherals/` — hardware peripherals (STM32, RPi GPIO); see `docs/hardware-peripherals-design.md`
- `src/runtime/` — runtime adapters (native, docker, wasm)
- `crates/robot-kit/` — standalone robotics toolkit crate
- `src/runtime/` — runtime adapters (currently native)
- `docs/` — task-oriented documentation system (hubs, unified TOC, references, operations, security proposals, multilingual guides)
- `dev/` — Docker-based dev environment (`cli.sh`) and local CI runner (`ci.sh`)
- `scripts/ci/` — CI gate scripts (quality gate, delta lint, security regression, docs checks)
- `tests/` — integration tests (e2e, channel routing, config, provider, webhook security)
- `benches/` — criterion benchmarks (`agent_benchmarks.rs`)
- `.github/` — CI, templates, automation workflows
## 4.1 Documentation System Contract (Required)
@ -304,8 +240,8 @@ All contributors (human or agent) must follow the same collaboration flow:
- Create and work from a non-`main` branch.
- Commit changes to that branch with clear, scoped commit messages.
- Open a PR to `main`; do not push directly to `main`.
- `main` is the integration branch for reviewed changes.
- Open a PR to `main` by default (`dev` is optional for integration batching); do not push directly to `dev` or `main`.
- `main` accepts direct PR merges after required checks and review policy pass.
- Wait for required checks and review outcomes before merging.
- Merge via PR controls (squash/rebase/merge as repository policy allows).
- After merge/close, clean up task branches/worktrees that are no longer needed.
@ -315,7 +251,7 @@ All contributors (human or agent) must follow the same collaboration flow:
- Decide merge/close outcomes from repository-local authority in this order: `.github/workflows/**`, GitHub branch protection/rulesets, `docs/pr-workflow.md`, then this `CLAUDE.md`.
- External agent skills/templates are execution aids only; they must not override repository-local policy.
- A normal contributor PR targeting `main` is expected; evaluate by intent, scope, and policy compliance.
- A normal contributor PR targeting `main` is valid under the main-first flow when required checks and review policy are satisfied; use `dev` only for explicit integration batching.
- Direct-close the PR (do not supersede/replay) when high-confidence integrity-risk signals exist:
- unapproved or unrelated repository rebranding attempts (for example replacing project logo/identity assets)
- unauthorized platform-surface expansion (for example introducing `web` apps, dashboards, frontend stacks, or UI surfaces not requested by maintainers)
@ -414,7 +350,6 @@ Use these rules to keep the trait/factory architecture stable under growth.
- Apply `docs/i18n-guide.md` completion checklist before merge and include i18n status in PR notes.
- For docs snapshots, add new date-stamped files for new sprints rather than rewriting historical context.
## 8) Validation Matrix
Default local checks for code changes:

705
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ resolver = "2"
[package]
name = "zeroclaw"
version = "0.1.7"
version = "0.1.9"
edition = "2021"
build = "build.rs"
authors = ["theonlyhennygod"]
@ -137,6 +137,8 @@ cron = "0.15"
dialoguer = { version = "0.12", features = ["fuzzy-select"] }
rustyline = "17.0"
console = "0.16"
crossterm = "0.29"
ratatui = { version = "0.29", default-features = false, features = ["crossterm"] }
# Hardware discovery (device path globbing)
glob = "0.3"

View File

@ -1,41 +1,51 @@
# syntax=docker/dockerfile:1.7
# ── Stage 1: Build ────────────────────────────────────────────
FROM rust:1.93-slim@sha256:9663b80a1621253d30b146454f903de48f0af925c967be48c84745537cd35d8b AS builder
FROM rust:1.93-slim@sha256:7e6fa79cf81be23fd45d857f75f583d80cfdbb11c91fa06180fd747fda37a61d AS builder
WORKDIR /app
ARG ZEROCLAW_CARGO_FEATURES=""
ARG ZEROCLAW_CARGO_ALL_FEATURES="false"
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y \
libudev-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# 1. Copy manifests to cache dependencies
COPY Cargo.toml Cargo.lock ./
COPY build.rs build.rs
COPY crates/robot-kit/Cargo.toml crates/robot-kit/Cargo.toml
COPY crates/zeroclaw-types/Cargo.toml crates/zeroclaw-types/Cargo.toml
COPY crates/zeroclaw-core/Cargo.toml crates/zeroclaw-core/Cargo.toml
# Create dummy targets declared in Cargo.toml so manifest parsing succeeds.
RUN mkdir -p src benches crates/robot-kit/src \
RUN mkdir -p src benches crates/robot-kit/src crates/zeroclaw-types/src crates/zeroclaw-core/src \
&& echo "fn main() {}" > src/main.rs \
&& echo "fn main() {}" > benches/agent_benchmarks.rs \
&& echo "pub fn placeholder() {}" > crates/robot-kit/src/lib.rs
&& echo "pub fn placeholder() {}" > crates/robot-kit/src/lib.rs \
&& echo "pub fn placeholder() {}" > crates/zeroclaw-types/src/lib.rs \
&& echo "pub fn placeholder() {}" > crates/zeroclaw-core/src/lib.rs
RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=zeroclaw-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=zeroclaw-target,target=/app/target,sharing=locked \
if [ -n "$ZEROCLAW_CARGO_FEATURES" ]; then \
if [ "$ZEROCLAW_CARGO_ALL_FEATURES" = "true" ]; then \
cargo build --release --locked --all-features; \
elif [ -n "$ZEROCLAW_CARGO_FEATURES" ]; then \
cargo build --release --locked --features "$ZEROCLAW_CARGO_FEATURES"; \
else \
cargo build --release --locked; \
fi
RUN rm -rf src benches crates/robot-kit/src
RUN rm -rf src benches crates/robot-kit/src crates/zeroclaw-types/src crates/zeroclaw-core/src
# 2. Copy only build-relevant source paths (avoid cache-busting on docs/tests/scripts)
COPY src/ src/
COPY benches/ benches/
COPY crates/ crates/
COPY firmware/ firmware/
COPY templates/ templates/
COPY web/ web/
# Keep release builds resilient when frontend dist assets are not prebuilt in Git.
RUN mkdir -p web/dist && \
@ -57,7 +67,9 @@ RUN mkdir -p web/dist && \
RUN --mount=type=cache,id=zeroclaw-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=zeroclaw-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=zeroclaw-target,target=/app/target,sharing=locked \
if [ -n "$ZEROCLAW_CARGO_FEATURES" ]; then \
if [ "$ZEROCLAW_CARGO_ALL_FEATURES" = "true" ]; then \
cargo build --release --locked --all-features; \
elif [ -n "$ZEROCLAW_CARGO_FEATURES" ]; then \
cargo build --release --locked --features "$ZEROCLAW_CARGO_FEATURES"; \
else \
cargo build --release --locked; \
@ -83,7 +95,7 @@ allow_public_bind = false
EOF
# ── Stage 2: Development Runtime (Debian) ────────────────────
FROM debian:trixie-slim@sha256:f6e2cfac5cf956ea044b4bd75e6397b4372ad88fe00908045e9a0d21712ae3ba AS dev
FROM debian:trixie-slim@sha256:1d3c811171a08a5adaa4a163fbafd96b61b87aa871bbc7aa15431ac275d3d430 AS dev
# Install essential runtime dependencies only (use docker-compose.override.yml for dev tools)
RUN apt-get update && apt-get install -y \

51
PR_DESCRIPTION_UPDATE.md Normal file
View File

@ -0,0 +1,51 @@
## Android Phase 3 - Agent Integration
This PR implements the Android client for ZeroClaw with full agent integration, including foreground service, Quick Settings tile, boot receiver, and background heartbeat support.
### Changes
- `ZeroClawApp.kt` - Application setup with notification channels and WorkManager
- `SettingsRepository.kt` - DataStore + EncryptedSharedPreferences for secure settings
- `SettingsScreen.kt` - Compose UI for configuring the agent
- `BootReceiver.kt` - Auto-start on boot when enabled
- `HeartbeatWorker.kt` - Background periodic tasks via WorkManager
- `ZeroClawTileService.kt` - Quick Settings tile for agent control
- `ShareHandler.kt` - Handle content shared from other apps
- `ci-android.yml` - GitHub Actions workflow for Android builds
- `proguard-rules.pro` - R8 optimization rules
---
## Validation Evidence
- [x] All HIGH and MEDIUM CodeRabbit issues addressed
- [x] DataStore IOException handling added to prevent crashes on corrupted preferences
- [x] BootReceiver double `pendingResult.finish()` call removed
- [x] `text/uri-list` MIME type routed correctly in ShareHandler
- [x] API 34+ PendingIntent overload added to TileService
- [x] Kotlin Intrinsics null checks preserved in ProGuard rules
- [x] HeartbeatWorker enforces 15-minute minimum and uses UPDATE policy
- [x] SettingsScreen refreshes battery optimization state on resume
- [x] ZeroClawApp listens for settings changes to update heartbeat schedule
- [x] Trailing whitespace removed from all Kotlin files
- [ ] Manual testing: Build and install on Android 14 device (pending)
## Security Impact
- **API Keys**: Stored in Android Keystore via EncryptedSharedPreferences (AES-256-GCM)
- **Permissions**: RECEIVE_BOOT_COMPLETED, FOREGROUND_SERVICE, POST_NOTIFICATIONS
- **Data in Transit**: All API calls use HTTPS
- **No New Vulnerabilities**: No raw SQL, no WebView JavaScript, no exported components without protection
## Privacy and Data Hygiene
- **Local Storage Only**: All settings stored on-device, nothing transmitted except to configured AI provider
- **No Analytics**: No third-party analytics or tracking SDKs
- **User Control**: API key can be cleared via settings, auto-start is opt-in
- **Minimal Permissions**: Only requests permissions necessary for core functionality
## Rollback Plan
1. **Feature Flag**: Not yet implemented; can be added if needed
2. **Version Pinning**: Users can stay on previous APK version
3. **Clean Uninstall**: All data stored in app's private directory, removed on uninstall
4. **Server-Side**: No backend changes required; rollback is client-only

View File

@ -1,884 +0,0 @@
<p align="center">
<img src="zeroclaw.png" alt="ZeroClaw" width="200" />
</p>
<h1 align="center">ZeroClaw 🦀</h1>
<p align="center">
<strong>Zéro surcharge. Zéro compromis. 100% Rust. 100% Agnostique.</strong><br>
⚡️ <strong>Fonctionne sur du matériel à 10$ avec <5 Mo de RAM : C'est 99% de mémoire en moins qu'OpenClaw et 98% moins cher qu'un Mac mini !</strong>
</p>
<p align="center">
<a href="LICENSE-APACHE"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache%202.0-blue.svg" alt="Licence : MIT ou Apache-2.0" /></a>
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributeurs" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Offrez-moi un café" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X : @zeroclawlabs" /></a>
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu : Officiel" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram : @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit : r/zeroclawlabs" /></a>
</p>
<p align="center">
Construit par des étudiants et membres des communautés Harvard, MIT et Sundai.Club.
</p>
<p align="center">
🌐 <strong>Langues :</strong> <a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ru.md">Русский</a> · <a href="README.fr.md">Français</a> · <a href="README.vi.md">Tiếng Việt</a>
</p>
<p align="center">
<a href="#démarrage-rapide">Démarrage</a> |
<a href="bootstrap.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>
<p align="center">
<strong>Accès rapides :</strong>
<a href="docs/reference/README.md">Référence</a> ·
<a href="docs/operations/README.md">Opérations</a> ·
<a href="docs/troubleshooting.md">Dépannage</a> ·
<a href="docs/security/README.md">Sécurité</a> ·
<a href="docs/hardware/README.md">Matériel</a> ·
<a href="docs/contributing/README.md">Contribuer</a>
</p>
<p align="center">
<strong>Infrastructure d'assistant IA rapide, légère et entièrement autonome</strong><br />
Déployez n'importe où. Échangez n'importe quoi.
</p>
<p align="center">
ZeroClaw est le <strong>système d'exploitation runtime</strong> pour les workflows agentiques — une infrastructure qui abstrait les modèles, outils, mémoire et exécution pour construire des agents une fois et les exécuter partout.
</p>
<p align="center"><code>Architecture pilotée par traits · runtime sécurisé par défaut · fournisseur/canal/outil interchangeables · tout est pluggable</code></p>
### 📢 Annonces
Utilisez ce tableau pour les avis importants (changements incompatibles, avis de sécurité, fenêtres de maintenance et bloqueurs de version).
| Date (UTC) | Niveau | Avis | Action |
| ---------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-02-19 | _Critique_ | Nous ne sommes **pas affiliés** à `openagen/zeroclaw` ou `zeroclaw.org`. Le domaine `zeroclaw.org` pointe actuellement vers le fork `openagen/zeroclaw`, et ce domaine/dépôt usurpe l'identité de notre site web/projet officiel. | Ne faites pas confiance aux informations, binaires, levées de fonds ou annonces provenant de ces sources. Utilisez uniquement [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) et nos comptes sociaux vérifiés. |
| 2026-02-21 | _Important_ | Notre site officiel est désormais en ligne : [zeroclawlabs.ai](https://zeroclawlabs.ai). Merci pour votre patience pendant cette attente. Nous constatons toujours des tentatives d'usurpation : ne participez à aucune activité d'investissement/financement au nom de ZeroClaw si elle n'est pas publiée via nos canaux officiels. | Utilisez [ce dépôt](https://github.com/zeroclaw-labs/zeroclaw) comme source unique de vérité. Suivez [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (groupe)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/), et [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) pour les mises à jour officielles. |
| 2026-02-19 | _Important_ | Anthropic a mis à jour les conditions d'utilisation de l'authentification et des identifiants le 2026-02-19. L'authentification OAuth (Free, Pro, Max) est exclusivement destinée à Claude Code et Claude.ai ; l'utilisation de tokens OAuth de Claude Free/Pro/Max dans tout autre produit, outil ou service (y compris Agent SDK) n'est pas autorisée et peut violer les Conditions d'utilisation grand public. | Veuillez temporairement éviter les intégrations OAuth de Claude Code pour prévenir toute perte potentielle. Clause originale : [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
### ✨ Fonctionnalités
- 🏎️ **Runtime Léger par Défaut :** Les workflows CLI courants et de statut s'exécutent dans une enveloppe mémoire de quelques mégaoctets sur les builds de production.
- 💰 **Déploiement Économique :** Conçu pour les cartes à faible coût et les petites instances cloud sans dépendances runtime lourdes.
- ⚡ **Démarrages à Froid Rapides :** Le runtime Rust mono-binaire maintient le démarrage des commandes et démons quasi instantané pour les opérations quotidiennes.
- 🌍 **Architecture Portable :** Un workflow binaire unique sur ARM, x86 et RISC-V avec fournisseurs/canaux/outils interchangeables.
### Pourquoi les équipes choisissent ZeroClaw
- **Léger par défaut :** petit binaire Rust, démarrage rapide, empreinte mémoire faible.
- **Sécurisé par conception :** appairage, sandboxing strict, listes d'autorisation explicites, portée de workspace.
- **Entièrement interchangeable :** les systèmes centraux sont des traits (fournisseurs, canaux, outils, mémoire, tunnels).
- **Aucun verrouillage :** support de fournisseur compatible OpenAI + endpoints personnalisés pluggables.
## Instantané de Benchmark (ZeroClaw vs OpenClaw, Reproductible)
Benchmark rapide sur machine locale (macOS arm64, fév. 2026) normalisé pour matériel edge 0.8 GHz.
| | OpenClaw | NanoBot | PicoClaw | ZeroClaw 🦀 |
| ---------------------------- | ------------- | -------------- | --------------- | --------------------- |
| **Langage** | TypeScript | Python | Go | **Rust** |
| **RAM** | > 1 Go | > 100 Mo | < 10 Mo | **< 5 Mo** |
| **Démarrage (cœur 0.8 GHz)** | > 500s | > 30s | < 1s | **< 10ms** |
| **Taille Binaire** | ~28 Mo (dist) | N/A (Scripts) | ~8 Mo | **3.4 Mo** |
| **Coût** | Mac Mini 599$ | Linux SBC ~50$ | Carte Linux 10$ | **Tout matériel 10$** |
> Notes : Les résultats ZeroClaw sont mesurés sur des builds de production utilisant `/usr/bin/time -l`. OpenClaw nécessite le runtime Node.js (typiquement ~390 Mo de surcharge mémoire supplémentaire), tandis que NanoBot nécessite le runtime Python. PicoClaw et ZeroClaw sont des binaires statiques. Les chiffres RAM ci-dessus sont la mémoire runtime ; les exigences de compilation build-time sont plus élevées.
<p align="center">
<img src="zero-claw.jpeg" alt="Comparaison ZeroClaw vs OpenClaw" width="800" />
</p>
### Mesure locale reproductible
Les affirmations de benchmark peuvent dériver au fil de l'évolution du code et des toolchains, donc mesurez toujours votre build actuel localement :
```bash
cargo build --release
ls -lh target/release/zeroclaw
/usr/bin/time -l target/release/zeroclaw --help
/usr/bin/time -l target/release/zeroclaw status
```
Exemple d'échantillon (macOS arm64, mesuré le 18 février 2026) :
- Taille binaire release : `8.8M`
- `zeroclaw --help` : environ `0.02s` de temps réel, ~`3.9 Mo` d'empreinte mémoire maximale
- `zeroclaw status` : environ `0.01s` de temps réel, ~`4.1 Mo` d'empreinte mémoire maximale
## Prérequis
<details>
<summary><strong>Windows</strong></summary>
### Windows — Requis
1. **Visual Studio Build Tools** (fournit le linker MSVC et le Windows SDK) :
```powershell
winget install Microsoft.VisualStudio.2022.BuildTools
```
Pendant l'installation (ou via le Visual Studio Installer), sélectionnez la charge de travail **"Développement Desktop en C++"**.
2. **Toolchain Rust :**
```powershell
winget install Rustlang.Rustup
```
Après l'installation, ouvrez un nouveau terminal et exécutez `rustup default stable` pour vous assurer que la toolchain stable est active.
3. **Vérifiez** que les deux fonctionnent :
```powershell
rustc --version
cargo --version
```
### Windows — Optionnel
- **Docker Desktop** — requis seulement si vous utilisez le [runtime sandboxé Docker](#support-runtime-actuel) (`runtime.kind = "docker"`). Installez via `winget install Docker.DockerDesktop`.
</details>
<details>
<summary><strong>Linux / macOS</strong></summary>
### Linux / macOS — Requis
1. **Outils de build essentiels :**
- **Linux (Debian/Ubuntu) :** `sudo apt install build-essential pkg-config`
- **Linux (Fedora/RHEL) :** `sudo dnf group install development-tools && sudo dnf install pkg-config`
- **macOS :** Installez les Outils de Ligne de Commande Xcode : `xcode-select --install`
2. **Toolchain Rust :**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
Voir [rustup.rs](https://rustup.rs) pour les détails.
3. **Vérifiez :**
```bash
rustc --version
cargo --version
```
### Linux / macOS — Optionnel
- **Docker** — requis seulement si vous utilisez le [runtime sandboxé Docker](#support-runtime-actuel) (`runtime.kind = "docker"`).
- **Linux (Debian/Ubuntu) :** voir [docs.docker.com](https://docs.docker.com/engine/install/ubuntu/)
- **Linux (Fedora/RHEL) :** voir [docs.docker.com](https://docs.docker.com/engine/install/fedora/)
- **macOS :** installez Docker Desktop via [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/)
</details>
## Démarrage Rapide
### Option 1 : Configuration automatisée (recommandée)
Le script `bootstrap.sh` installe Rust, clone ZeroClaw, le compile, et configure votre environnement de développement initial :
```bash
curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/main/bootstrap.sh | bash
```
Ceci va :
1. Installer Rust (si absent)
2. Cloner le dépôt ZeroClaw
3. Compiler ZeroClaw en mode release
4. Installer `zeroclaw` dans `~/.cargo/bin/`
5. Créer la structure de workspace par défaut dans `~/.zeroclaw/workspace/`
6. Générer un fichier de configuration `~/.zeroclaw/workspace/config.toml` de démarrage
Après le bootstrap, relancez votre shell ou exécutez `source ~/.cargo/env` pour utiliser la commande `zeroclaw` globalement.
### Option 2 : Installation manuelle
<details>
<summary><strong>Cliquez pour voir les étapes d'installation manuelle</strong></summary>
```bash
# 1. Clonez le dépôt
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
# 2. Compilez en release
cargo build --release --locked
# 3. Installez le binaire
cargo install --path . --locked
# 4. Initialisez le workspace
zeroclaw init
# 5. Vérifiez l'installation
zeroclaw --version
zeroclaw status
```
</details>
### Après l'installation
Une fois installé (via bootstrap ou manuellement), vous devriez voir :
```
~/.zeroclaw/workspace/
├── config.toml # Configuration principale
├── .pairing # Secrets de pairing (généré au premier lancement)
├── logs/ # Journaux de daemon/agent
├── skills/ # Compétences personnalisées
└── memory/ # Stockage de contexte conversationnel
```
**Prochaines étapes :**
1. Configurez vos fournisseurs d'IA dans `~/.zeroclaw/workspace/config.toml`
2. Consultez la [référence de configuration](docs/config-reference.md) pour les options avancées
3. Lancez l'agent : `zeroclaw agent start`
4. Testez via votre canal préféré (voir [référence des canaux](docs/channels-reference.md))
## Configuration
Éditez `~/.zeroclaw/workspace/config.toml` pour configurer les fournisseurs, canaux et comportement du système.
### Référence de Configuration Rapide
```toml
[providers.anthropic]
api_key = "sk-ant-..."
model = "claude-sonnet-4-20250514"
[providers.openai]
api_key = "sk-..."
model = "gpt-4o"
[channels.telegram]
enabled = true
bot_token = "123456:ABC-DEF..."
[channels.matrix]
enabled = true
homeserver_url = "https://matrix.org"
username = "@bot:matrix.org"
password = "..."
[memory]
kind = "markdown" # ou "sqlite" ou "none"
[runtime]
kind = "native" # ou "docker" (nécessite Docker)
```
**Documents de référence complets :**
- [Référence de Configuration](docs/config-reference.md) — tous les paramètres, validations, valeurs par défaut
- [Référence des Fournisseurs](docs/providers-reference.md) — configurations spécifiques aux fournisseurs d'IA
- [Référence des Canaux](docs/channels-reference.md) — Telegram, Matrix, Slack, Discord et plus
- [Opérations](docs/operations-runbook.md) — surveillance en production, rotation des secrets, mise à l'échelle
### Support Runtime (actuel)
ZeroClaw prend en charge deux backends d'exécution de code :
- **`native`** (par défaut) — exécution de processus directe, chemin le plus rapide, idéal pour les environnements de confiance
- **`docker`** — isolation complète du conteneur, politiques de sécurité renforcées, nécessite Docker
Utilisez `runtime.kind = "docker"` si vous avez besoin d'un sandboxing strict ou de l'isolation réseau. Voir [référence de configuration](docs/config-reference.md#runtime) pour les détails complets.
## Commandes
```bash
# Gestion du workspace
zeroclaw init # Initialise un nouveau workspace
zeroclaw status # Affiche l'état du daemon/agent
zeroclaw config validate # Vérifie la syntaxe et les valeurs de config.toml
# Gestion du daemon
zeroclaw daemon start # Démarre le daemon en arrière-plan
zeroclaw daemon stop # Arrête le daemon en cours d'exécution
zeroclaw daemon restart # Redémarre le daemon (rechargement de config)
zeroclaw daemon logs # Affiche les journaux du daemon
# Gestion de l'agent
zeroclaw agent start # Démarre l'agent (nécessite daemon en cours d'exécution)
zeroclaw agent stop # Arrête l'agent
zeroclaw agent restart # Redémarre l'agent (rechargement de config)
# Opérations de pairing
zeroclaw pairing init # Génère un nouveau secret de pairing
zeroclaw pairing rotate # Fait tourner le secret de pairing existant
# Tunneling (pour exposition publique)
zeroclaw tunnel start # Démarre un tunnel vers le daemon local
zeroclaw tunnel stop # Arrête le tunnel actif
# Diagnostic
zeroclaw doctor # Exécute les vérifications de santé du système
zeroclaw version # Affiche la version et les informations de build
```
Voir [Référence des Commandes](docs/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/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/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/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/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/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/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/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/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/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 :
- Hub de documentation : [`docs/README.md`](docs/README.md)
- Table des matières unifiée docs : [`docs/SUMMARY.md`](docs/SUMMARY.md)
- Référence des commandes : [`docs/commands-reference.md`](docs/commands-reference.md)
- Référence de configuration : [`docs/config-reference.md`](docs/config-reference.md)
- Référence des fournisseurs : [`docs/providers-reference.md`](docs/providers-reference.md)
- Référence des canaux : [`docs/channels-reference.md`](docs/channels-reference.md)
- Runbook des opérations : [`docs/operations-runbook.md`](docs/operations-runbook.md)
- Dépannage : [`docs/troubleshooting.md`](docs/troubleshooting.md)
- Inventaire/classification docs : [`docs/docs-inventory.md`](docs/docs-inventory.md)
- Instantané triage PR/Issue (au 18 février 2026) : [`docs/project-triage-snapshot-2026-02-18.md`](docs/project-triage-snapshot-2026-02-18.md)
Références de collaboration principales :
- Hub de documentation : [docs/README.md](docs/README.md)
- Modèle de documentation : [docs/doc-template.md](docs/doc-template.md)
- Checklist de modification de documentation : [docs/README.md#4-documentation-change-checklist](docs/README.md#4-documentation-change-checklist)
- Référence de configuration des canaux : [docs/channels-reference.md](docs/channels-reference.md)
- Opérations de salles chiffrées Matrix : [docs/matrix-e2ee-guide.md](docs/matrix-e2ee-guide.md)
- Guide de contribution : [CONTRIBUTING.md](CONTRIBUTING.md)
- Politique de workflow PR : [docs/pr-workflow.md](docs/pr-workflow.md)
- Playbook du relecteur (triage + revue approfondie) : [docs/reviewer-playbook.md](docs/reviewer-playbook.md)
- Carte de propriété et triage CI : [docs/ci-map.md](docs/ci-map.md)
- Politique de divulgation de sécurité : [SECURITY.md](SECURITY.md)
Pour le déploiement et les opérations runtime :
- Guide de déploiement réseau : [docs/network-deployment.md](docs/network-deployment.md)
- Playbook d'agent proxy : [docs/proxy-agent-playbook.md](docs/proxy-agent-playbook.md)
## Soutenir ZeroClaw
Si ZeroClaw aide votre travail et que vous souhaitez soutenir le développement continu, vous pouvez faire un don ici :
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=for-the-badge&logo=buy-me-a-coffee" alt="Offrez-moi un café" /></a>
### 🙏 Remerciements Spéciaux
Un remerciement sincère aux communautés et institutions qui inspirent et alimentent ce travail open-source :
- **Harvard University** — pour favoriser la curiosité intellectuelle et repousser les limites du possible.
- **MIT** — pour défendre la connaissance ouverte, l'open source, et la conviction que la technologie devrait être accessible à tous.
- **Sundai Club** — pour la communauté, l'énergie, et la volonté incessante de construire des choses qui comptent.
- **Le Monde & Au-Delà** 🌍✨ — à chaque contributeur, rêveur, et constructeur là-bas qui fait de l'open source une force pour le bien. C'est pour vous.
Nous construisons en open source parce que les meilleures idées viennent de partout. Si vous lisez ceci, vous en faites partie. Bienvenue. 🦀❤️
## ⚠️ Dépôt Officiel & Avertissement d'Usurpation d'Identité
**Ceci est le seul dépôt officiel ZeroClaw :**
> <https://github.com/zeroclaw-labs/zeroclaw>
Tout autre dépôt, organisation, domaine ou package prétendant être "ZeroClaw" ou impliquant une affiliation avec ZeroClaw Labs est **non autorisé et non affilié à ce projet**. Les forks non autorisés connus seront listés dans [TRADEMARK.md](TRADEMARK.md).
Si vous rencontrez une usurpation d'identité ou une utilisation abusive de marque, veuillez [ouvrir une issue](https://github.com/zeroclaw-labs/zeroclaw/issues).
---
## Licence
ZeroClaw est sous double licence pour une ouverture maximale et la protection des contributeurs :
| Licence | Cas d'utilisation |
| ---------------------------- | ------------------------------------------------------------ |
| [MIT](LICENSE-MIT) | Open-source, recherche, académique, usage personnel |
| [Apache 2.0](LICENSE-APACHE) | Protection de brevet, institutionnel, déploiement commercial |
Vous pouvez choisir l'une ou l'autre licence. **Les contributeurs accordent automatiquement des droits sous les deux** — voir [CLA.md](CLA.md) pour l'accord de contributeur complet.
### Marque
Le nom **ZeroClaw** et le logo sont des marques déposées de ZeroClaw Labs. Cette licence n'accorde pas la permission de les utiliser pour impliquer une approbation ou une affiliation. Voir [TRADEMARK.md](TRADEMARK.md) pour les utilisations permises et interdites.
### Protections des Contributeurs
- Vous **conservez les droits d'auteur** de vos contributions
- **Concession de brevet** (Apache 2.0) vous protège contre les réclamations de brevet par d'autres contributeurs
- Vos contributions sont **attribuées de manière permanente** dans l'historique des commits et [NOTICE](NOTICE)
- Aucun droit de marque n'est transféré en contribuant
## Contribuer
Voir [CONTRIBUTING.md](CONTRIBUTING.md) et [CLA.md](CLA.md). Implémentez un trait, soumettez une PR :
- Guide de workflow CI : [docs/ci-map.md](docs/ci-map.md)
- Nouveau `Provider``src/providers/`
- Nouveau `Channel``src/channels/`
- Nouveau `Observer``src/observability/`
- Nouveau `Tool``src/tools/`
- Nouvelle `Memory``src/memory/`
- Nouveau `Tunnel``src/tunnel/`
- Nouvelle `Skill``~/.zeroclaw/workspace/skills/<n>/`
---
**ZeroClaw** — Zéro surcharge. Zéro compromis. Déployez n'importe où. Échangez n'importe quoi. 🦀
## Historique des Étoiles
<p align="center">
<a href="https://www.star-history.com/#zeroclaw-labs/zeroclaw&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=zeroclaw-labs/zeroclaw&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=zeroclaw-labs/zeroclaw&type=date&legend=top-left" />
<img alt="Graphique Historique des Étoiles" src="https://api.star-history.com/svg?repos=zeroclaw-labs/zeroclaw&type=date&legend=top-left" />
</picture>
</a>
</p>

View File

@ -1,300 +0,0 @@
<p align="center">
<img src="zeroclaw.png" alt="ZeroClaw" width="200" />
</p>
<h1 align="center">ZeroClaw 🦀(日本語)</h1>
<p align="center">
<strong>Zero overhead. Zero compromise. 100% Rust. 100% Agnostic.</strong>
</p>
<p align="center">
<a href="LICENSE-APACHE"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache%202.0-blue.svg" alt="License: MIT OR Apache-2.0" /></a>
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
🌐 言語: <a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ru.md">Русский</a> · <a href="README.fr.md">Français</a> · <a href="README.vi.md">Tiếng Việt</a>
</p>
<p align="center">
<a href="bootstrap.sh">ワンクリック導入</a> |
<a href="docs/getting-started/README.md">導入ガイド</a> |
<a href="docs/README.ja.md">ドキュメントハブ</a> |
<a href="docs/SUMMARY.md">Docs TOC</a>
</p>
<p align="center">
<strong>クイック分流:</strong>
<a href="docs/reference/README.md">参照</a> ·
<a href="docs/operations/README.md">運用</a> ·
<a href="docs/troubleshooting.md">障害対応</a> ·
<a href="docs/security/README.md">セキュリティ</a> ·
<a href="docs/hardware/README.md">ハードウェア</a> ·
<a href="docs/contributing/README.md">貢献・CI</a>
</p>
> この文書は `README.md` の内容を、正確性と可読性を重視して日本語に整えた版です(逐語訳ではありません)。
>
> コマンド名、設定キー、API パス、Trait 名などの技術識別子は英語のまま維持しています。
>
> 最終同期日: **2026-02-19**
## 📢 お知らせボード
重要なお知らせ(互換性破壊変更、セキュリティ告知、メンテナンス時間、リリース阻害事項など)をここに掲載します。
| 日付 (UTC) | レベル | お知らせ | 対応 |
|---|---|---|---|
| 2026-02-19 | _緊急_ | 私たちは `openagen/zeroclaw` および `zeroclaw.org` とは**一切関係ありません**。`zeroclaw.org` は現在 `openagen/zeroclaw` の fork を指しており、そのドメイン/リポジトリは当プロジェクトの公式サイト・公式プロジェクトを装っています。 | これらの情報源による案内、バイナリ、資金調達情報、公式発表は信頼しないでください。必ず[本リポジトリ](https://github.com/zeroclaw-labs/zeroclaw)と認証済み公式SNSのみを参照してください。 |
| 2026-02-21 | _重要_ | 公式サイトを公開しました: [zeroclawlabs.ai](https://zeroclawlabs.ai)。公開までお待ちいただきありがとうございました。引き続きなりすましの試みを確認しているため、ZeroClaw 名義の投資・資金調達などの案内は、公式チャネルで確認できない限り参加しないでください。 | 情報は[本リポジトリ](https://github.com/zeroclaw-labs/zeroclaw)を最優先で確認し、[X@zeroclawlabs](https://x.com/zeroclawlabs?s=21)、[Telegram@zeroclawlabs](https://t.me/zeroclawlabs)、[Facebookグループ](https://www.facebook.com/groups/zeroclaw)、[Redditr/zeroclawlabs](https://www.reddit.com/r/zeroclawlabs/) と [小紅書アカウント](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) で公式更新を確認してください。 |
| 2026-02-19 | _重要_ | Anthropic は 2026-02-19 に Authentication and Credential Use を更新しました。条文では、OAuth authenticationFree/Pro/Maxは Claude Code と Claude.ai 専用であり、Claude Free/Pro/Max で取得した OAuth トークンを他の製品・ツール・サービスAgent SDK を含むで使用することは許可されず、Consumer Terms of Service 違反に該当すると明記されています。 | 損失回避のため、当面は Claude Code OAuth 連携を試さないでください。原文: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use)。 |
## 概要
ZeroClaw は、高速・省リソース・高拡張性を重視した自律エージェント実行基盤です。ZeroClawはエージェントワークフローのための**ランタイムオペレーティングシステム**です — モデル、ツール、メモリ、実行を抽象化し、エージェントを一度構築すればどこでも実行できるインフラストラクチャです。
- Rust ネイティブ実装、単一バイナリで配布可能
- Trait ベース設計(`Provider` / `Channel` / `Tool` / `Memory` など)
- セキュアデフォルト(ペアリング、明示 allowlist、サンドボックス、スコープ制御
## ZeroClaw が選ばれる理由
- **軽量ランタイムを標準化**: CLI や `status` などの常用操作は数MB級メモリで動作。
- **低コスト環境に適合**: 低価格ボードや小規模クラウドでも、重い実行基盤なしで運用可能。
- **高速コールドスタート**: Rust 単一バイナリにより、主要コマンドと daemon 起動が非常に速い。
- **高い移植性**: ARM / x86 / RISC-V を同じ運用モデルで扱え、provider/channel/tool を差し替え可能。
## ベンチマークスナップショットZeroClaw vs OpenClaw、再現可能
以下はローカルのクイック比較macOS arm64、2026年2月を、0.8GHz エッジ CPU 基準で正規化したものです。
| | OpenClaw | NanoBot | PicoClaw | ZeroClaw 🦀 |
|---|---|---|---|---|
| **言語** | TypeScript | Python | Go | **Rust** |
| **RAM** | > 1GB | > 100MB | < 10MB | **< 5MB** |
| **起動時間0.8GHz コア)** | > 500s | > 30s | < 1s | **< 10ms** |
| **バイナリサイズ** | ~28MBdist | N/Aスクリプト | ~8MB | **~8.8 MB** |
| **コスト** | Mac Mini $599 | Linux SBC ~$50 | Linux ボード $10 | **任意の $10 ハードウェア** |
> 注記: ZeroClaw の結果は release ビルドを `/usr/bin/time -l` で計測したものです。OpenClaw は Node.js ランタイムが必要で、ランタイム由来だけで通常は約390MBの追加メモリを要します。NanoBot は Python ランタイムが必要です。PicoClaw と ZeroClaw は静的バイナリです。
<p align="center">
<img src="zero-claw.jpeg" alt="ZeroClaw vs OpenClaw Comparison" width="800" />
</p>
### ローカルで再現可能な測定
ベンチマーク値はコードやツールチェーン更新で変わるため、必ず自身の環境で再測定してください。
```bash
cargo build --release
ls -lh target/release/zeroclaw
/usr/bin/time -l target/release/zeroclaw --help
/usr/bin/time -l target/release/zeroclaw status
```
README のサンプル値macOS arm64, 2026-02-18:
- Release バイナリ: `8.8M`
- `zeroclaw --help`: 約 `0.02s`、ピークメモリ 約 `3.9MB`
- `zeroclaw status`: 約 `0.01s`、ピークメモリ 約 `4.1MB`
## ワンクリック導入
```bash
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
./bootstrap.sh
```
環境ごと初期化する場合: `./bootstrap.sh --install-system-deps --install-rust`(システムパッケージで `sudo` が必要な場合があります)。
詳細は [`docs/one-click-bootstrap.md`](docs/one-click-bootstrap.md) を参照してください。
## クイックスタート
### HomebrewmacOS/Linuxbrew
```bash
brew install zeroclaw
```
```bash
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
cargo build --release --locked
cargo install --path . --force --locked
zeroclaw onboard --api-key sk-... --provider openrouter
zeroclaw onboard --interactive
zeroclaw agent -m "Hello, ZeroClaw!"
# default: 127.0.0.1:42617
zeroclaw gateway
zeroclaw daemon
```
## Subscription AuthOpenAI Codex / Claude Code
ZeroClaw はサブスクリプションベースのネイティブ認証プロファイルをサポートしています(マルチアカウント対応、保存時暗号化)。
- 保存先: `~/.zeroclaw/auth-profiles.json`
- 暗号化キー: `~/.zeroclaw/.secret_key`
- Profile ID 形式: `<provider>:<profile_name>`(例: `openai-codex:work`
OpenAI Codex OAuthChatGPT サブスクリプション):
```bash
# サーバー/ヘッドレス環境向け推奨
zeroclaw auth login --provider openai-codex --device-code
# ブラウザ/コールバックフロー(ペーストフォールバック付き)
zeroclaw auth login --provider openai-codex --profile default
zeroclaw auth paste-redirect --provider openai-codex --profile default
# 確認 / リフレッシュ / プロファイル切替
zeroclaw auth status
zeroclaw auth refresh --provider openai-codex --profile default
zeroclaw auth use --provider openai-codex --profile work
```
Claude Code / Anthropic setup-token:
```bash
# サブスクリプション/setup token の貼り付けAuthorization header モード)
zeroclaw auth paste-token --provider anthropic --profile default --auth-kind authorization
# エイリアスコマンド
zeroclaw auth setup-token --provider anthropic --profile default
```
Subscription auth で agent を実行:
```bash
zeroclaw agent --provider openai-codex -m "hello"
zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hello"
# Anthropic は API key と auth token の両方の環境変数をサポート:
# ANTHROPIC_AUTH_TOKEN, ANTHROPIC_OAUTH_TOKEN, ANTHROPIC_API_KEY
zeroclaw agent --provider anthropic -m "hello"
```
## アーキテクチャ
すべてのサブシステムは **Trait** — 設定変更だけで実装を差し替え可能、コード変更不要。
<p align="center">
<img src="docs/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/commands-reference.md`](docs/commands-reference.md)
- 設定リファレンス: [`docs/config-reference.md`](docs/config-reference.md)
- Provider リファレンス: [`docs/providers-reference.md`](docs/providers-reference.md)
- Channel リファレンス: [`docs/channels-reference.md`](docs/channels-reference.md)
- 運用ガイドRunbook: [`docs/operations-runbook.md`](docs/operations-runbook.md)
- トラブルシューティング: [`docs/troubleshooting.md`](docs/troubleshooting.md)
- ドキュメント一覧 / 分類: [`docs/docs-inventory.md`](docs/docs-inventory.md)
- プロジェクト triage スナップショット: [`docs/project-triage-snapshot-2026-02-18.md`](docs/project-triage-snapshot-2026-02-18.md)
## コントリビュート / ライセンス
- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
- PR Workflow: [`docs/pr-workflow.md`](docs/pr-workflow.md)
- Reviewer Playbook: [`docs/reviewer-playbook.md`](docs/reviewer-playbook.md)
- License: MIT or Apache 2.0[`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE), [`NOTICE`](NOTICE)
---
詳細仕様全コマンド、アーキテクチャ、API 仕様、開発フロー)は英語版の [`README.md`](README.md) を参照してください。

1126
README.md

File diff suppressed because it is too large Load Diff

View File

@ -1,300 +0,0 @@
<p align="center">
<img src="zeroclaw.png" alt="ZeroClaw" width="200" />
</p>
<h1 align="center">ZeroClaw 🦀(Русский)</h1>
<p align="center">
<strong>Zero overhead. Zero compromise. 100% Rust. 100% Agnostic.</strong>
</p>
<p align="center">
<a href="LICENSE-APACHE"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache%202.0-blue.svg" alt="License: MIT OR Apache-2.0" /></a>
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
🌐 Языки: <a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ru.md">Русский</a> · <a href="README.fr.md">Français</a> · <a href="README.vi.md">Tiếng Việt</a>
</p>
<p align="center">
<a href="bootstrap.sh">Установка в 1 клик</a> |
<a href="docs/getting-started/README.md">Быстрый старт</a> |
<a href="docs/README.ru.md">Хаб документации</a> |
<a href="docs/SUMMARY.md">TOC docs</a>
</p>
<p align="center">
<strong>Быстрые маршруты:</strong>
<a href="docs/reference/README.md">Справочники</a> ·
<a href="docs/operations/README.md">Операции</a> ·
<a href="docs/troubleshooting.md">Диагностика</a> ·
<a href="docs/security/README.md">Безопасность</a> ·
<a href="docs/hardware/README.md">Аппаратная часть</a> ·
<a href="docs/contributing/README.md">Вклад и CI</a>
</p>
> Этот файл — выверенный перевод `README.md` с акцентом на точность и читаемость (не дословный перевод).
>
> Технические идентификаторы (команды, ключи конфигурации, API-пути, имена Trait) сохранены на английском.
>
> Последняя синхронизация: **2026-02-19**.
## 📢 Доска объявлений
Публикуйте здесь важные уведомления (breaking changes, security advisories, окна обслуживания и блокеры релиза).
| Дата (UTC) | Уровень | Объявление | Действие |
|---|---|---|---|
| 2026-02-19 | _Срочно_ | Мы **не аффилированы** с `openagen/zeroclaw` и `zeroclaw.org`. Домен `zeroclaw.org` сейчас указывает на fork `openagen/zeroclaw`, и этот домен/репозиторий выдают себя за наш официальный сайт и проект. | Не доверяйте информации, бинарникам, сборам средств и «официальным» объявлениям из этих источников. Используйте только [этот репозиторий](https://github.com/zeroclaw-labs/zeroclaw) и наши верифицированные соцсети. |
| 2026-02-21 | _Важно_ | Наш официальный сайт уже запущен: [zeroclawlabs.ai](https://zeroclawlabs.ai). Спасибо, что дождались запуска. При этом попытки выдавать себя за ZeroClaw продолжаются, поэтому не участвуйте в инвестициях, сборах средств и похожих активностях, если они не подтверждены через наши официальные каналы. | Ориентируйтесь только на [этот репозиторий](https://github.com/zeroclaw-labs/zeroclaw); также следите за [X (@zeroclawlabs)](https://x.com/zeroclawlabs?s=21), [Telegram (@zeroclawlabs)](https://t.me/zeroclawlabs), [Facebook (группа)](https://www.facebook.com/groups/zeroclaw), [Reddit (r/zeroclawlabs)](https://www.reddit.com/r/zeroclawlabs/) и [Xiaohongshu](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) для официальных обновлений. |
| 2026-02-19 | _Важно_ | Anthropic обновил раздел Authentication and Credential Use 2026-02-19. В нем указано, что OAuth authentication (Free/Pro/Max) предназначена только для Claude Code и Claude.ai; использование OAuth-токенов, полученных через Claude Free/Pro/Max, в любых других продуктах, инструментах или сервисах (включая Agent SDK), не допускается и может считаться нарушением Consumer Terms of Service. | Чтобы избежать потерь, временно не используйте Claude Code OAuth-интеграции. Оригинал: [Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use). |
## О проекте
ZeroClaw — это производительная и расширяемая инфраструктура автономного AI-агента. ZeroClaw — это **операционная система времени выполнения** для агентных рабочих процессов — инфраструктура, абстрагирующая модели, инструменты, память и выполнение, позволяя создавать агентов один раз и запускать где угодно.
- Нативно на Rust, единый бинарник, переносимость между ARM / x86 / RISC-V
- Архитектура на Trait (`Provider`, `Channel`, `Tool`, `Memory` и др.)
- Безопасные значения по умолчанию: pairing, явные allowlist, sandbox и scope-ограничения
## Почему выбирают ZeroClaw
- **Лёгкий runtime по умолчанию**: Повседневные CLI-операции и `status` обычно укладываются в несколько МБ памяти.
- **Оптимизирован для недорогих сред**: Подходит для бюджетных плат и небольших cloud-инстансов без тяжёлой runtime-обвязки.
- **Быстрый cold start**: Архитектура одного Rust-бинарника ускоряет запуск основных команд и daemon-режима.
- **Портативная модель деплоя**: Единый подход для ARM / x86 / RISC-V и возможность менять providers/channels/tools.
## Снимок бенчмарка (ZeroClaw vs OpenClaw, воспроизводимо)
Ниже — быстрый локальный сравнительный срез (macOS arm64, февраль 2026), нормализованный под 0.8GHz edge CPU.
| | OpenClaw | NanoBot | PicoClaw | ZeroClaw 🦀 |
|---|---|---|---|---|
| **Язык** | TypeScript | Python | Go | **Rust** |
| **RAM** | > 1GB | > 100MB | < 10MB | **< 5MB** |
| **Старт (ядро 0.8GHz)** | > 500s | > 30s | < 1s | **< 10ms** |
| **Размер бинарника** | ~28MB (dist) | N/A (скрипты) | ~8MB | **~8.8 MB** |
| **Стоимость** | Mac Mini $599 | Linux SBC ~$50 | Linux-плата $10 | **Любое железо за $10** |
> Примечание: результаты ZeroClaw получены на release-сборке с помощью `/usr/bin/time -l`. OpenClaw требует Node.js runtime; только этот runtime обычно добавляет около 390MB дополнительного потребления памяти. NanoBot требует Python runtime. PicoClaw и ZeroClaw — статические бинарники.
<p align="center">
<img src="zero-claw.jpeg" alt="Сравнение ZeroClaw и OpenClaw" width="800" />
</p>
### Локально воспроизводимое измерение
Метрики могут меняться вместе с кодом и toolchain, поэтому проверяйте результаты в своей среде:
```bash
cargo build --release
ls -lh target/release/zeroclaw
/usr/bin/time -l target/release/zeroclaw --help
/usr/bin/time -l target/release/zeroclaw status
```
Текущие примерные значения из README (macOS arm64, 2026-02-18):
- Размер release-бинарника: `8.8M`
- `zeroclaw --help`: ~`0.02s`, пик памяти ~`3.9MB`
- `zeroclaw status`: ~`0.01s`, пик памяти ~`4.1MB`
## Установка в 1 клик
```bash
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
./bootstrap.sh
```
Для полной инициализации окружения: `./bootstrap.sh --install-system-deps --install-rust` (для системных пакетов может потребоваться `sudo`).
Подробности: [`docs/one-click-bootstrap.md`](docs/one-click-bootstrap.md).
## Быстрый старт
### Homebrew (macOS/Linuxbrew)
```bash
brew install zeroclaw
```
```bash
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
cargo build --release --locked
cargo install --path . --force --locked
zeroclaw onboard --api-key sk-... --provider openrouter
zeroclaw onboard --interactive
zeroclaw agent -m "Hello, ZeroClaw!"
# default: 127.0.0.1:42617
zeroclaw gateway
zeroclaw daemon
```
## Subscription Auth (OpenAI Codex / Claude Code)
ZeroClaw поддерживает нативные профили авторизации на основе подписки (мультиаккаунт, шифрование при хранении).
- Файл хранения: `~/.zeroclaw/auth-profiles.json`
- Ключ шифрования: `~/.zeroclaw/.secret_key`
- Формат Profile ID: `<provider>:<profile_name>` (пример: `openai-codex:work`)
OpenAI Codex OAuth (подписка ChatGPT):
```bash
# Рекомендуется для серверов/headless-окружений
zeroclaw auth login --provider openai-codex --device-code
# Браузерный/callback-поток с paste-фолбэком
zeroclaw auth login --provider openai-codex --profile default
zeroclaw auth paste-redirect --provider openai-codex --profile default
# Проверка / обновление / переключение профиля
zeroclaw auth status
zeroclaw auth refresh --provider openai-codex --profile default
zeroclaw auth use --provider openai-codex --profile work
```
Claude Code / Anthropic setup-token:
```bash
# Вставка subscription/setup token (режим Authorization header)
zeroclaw auth paste-token --provider anthropic --profile default --auth-kind authorization
# Команда-алиас
zeroclaw auth setup-token --provider anthropic --profile default
```
Запуск agent с subscription auth:
```bash
zeroclaw agent --provider openai-codex -m "hello"
zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hello"
# Anthropic поддерживает и API key, и auth token через переменные окружения:
# ANTHROPIC_AUTH_TOKEN, ANTHROPIC_OAUTH_TOKEN, ANTHROPIC_API_KEY
zeroclaw agent --provider anthropic -m "hello"
```
## Архитектура
Каждая подсистема — это **Trait**: меняйте реализации через конфигурацию, без изменения кода.
<p align="center">
<img src="docs/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/commands-reference.md`](docs/commands-reference.md)
- Справочник конфигурации: [`docs/config-reference.md`](docs/config-reference.md)
- Справочник providers: [`docs/providers-reference.md`](docs/providers-reference.md)
- Справочник channels: [`docs/channels-reference.md`](docs/channels-reference.md)
- Операционный runbook: [`docs/operations-runbook.md`](docs/operations-runbook.md)
- Устранение неполадок: [`docs/troubleshooting.md`](docs/troubleshooting.md)
- Инвентарь и классификация docs: [`docs/docs-inventory.md`](docs/docs-inventory.md)
- Снимок triage проекта: [`docs/project-triage-snapshot-2026-02-18.md`](docs/project-triage-snapshot-2026-02-18.md)
## Вклад и лицензия
- Contribution guide: [`CONTRIBUTING.md`](CONTRIBUTING.md)
- PR workflow: [`docs/pr-workflow.md`](docs/pr-workflow.md)
- Reviewer playbook: [`docs/reviewer-playbook.md`](docs/reviewer-playbook.md)
- License: MIT or Apache 2.0 ([`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE), [`NOTICE`](NOTICE))
---
Для полной и исчерпывающей информации (архитектура, все команды, API, разработка) используйте основной английский документ: [`README.md`](README.md).

File diff suppressed because it is too large Load Diff

View File

@ -1,305 +0,0 @@
<p align="center">
<img src="zeroclaw.png" alt="ZeroClaw" width="200" />
</p>
<h1 align="center">ZeroClaw 🦀(简体中文)</h1>
<p align="center">
<strong>零开销、零妥协;随处部署、万物可换。</strong>
</p>
<p align="center">
<a href="LICENSE-APACHE"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache%202.0-blue.svg" alt="License: MIT OR Apache-2.0" /></a>
<a href="NOTICE"><img src="https://img.shields.io/badge/contributors-27+-green.svg" alt="Contributors" /></a>
<a href="https://buymeacoffee.com/argenistherose"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Donate-yellow.svg?style=flat&logo=buy-me-a-coffee" alt="Buy Me a Coffee" /></a>
<a href="https://x.com/zeroclawlabs?s=21"><img src="https://img.shields.io/badge/X-%40zeroclawlabs-000000?style=flat&logo=x&logoColor=white" alt="X: @zeroclawlabs" /></a>
<a href="https://zeroclawlabs.cn/group.jpg"><img src="https://img.shields.io/badge/WeChat-Group-B7D7A8?logo=wechat&logoColor=white" alt="WeChat Group" /></a>
<a href="https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search"><img src="https://img.shields.io/badge/Xiaohongshu-Official-FF2442?style=flat" alt="Xiaohongshu: Official" /></a>
<a href="https://t.me/zeroclawlabs"><img src="https://img.shields.io/badge/Telegram-%40zeroclawlabs-26A5E4?style=flat&logo=telegram&logoColor=white" alt="Telegram: @zeroclawlabs" /></a>
<a href="https://www.facebook.com/groups/zeroclaw"><img src="https://img.shields.io/badge/Facebook-Group-1877F2?style=flat&logo=facebook&logoColor=white" alt="Facebook Group" /></a>
<a href="https://www.reddit.com/r/zeroclawlabs/"><img src="https://img.shields.io/badge/Reddit-r%2Fzeroclawlabs-FF4500?style=flat&logo=reddit&logoColor=white" alt="Reddit: r/zeroclawlabs" /></a>
</p>
<p align="center">
🌐 语言:<a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ru.md">Русский</a> · <a href="README.fr.md">Français</a> · <a href="README.vi.md">Tiếng Việt</a>
</p>
<p align="center">
<a href="bootstrap.sh">一键部署</a> |
<a href="docs/getting-started/README.md">安装入门</a> |
<a href="docs/README.zh-CN.md">文档总览</a> |
<a href="docs/SUMMARY.md">文档目录</a>
</p>
<p align="center">
<strong>场景分流:</strong>
<a href="docs/reference/README.md">参考手册</a> ·
<a href="docs/operations/README.md">运维部署</a> ·
<a href="docs/troubleshooting.md">故障排查</a> ·
<a href="docs/security/README.md">安全专题</a> ·
<a href="docs/hardware/README.md">硬件外设</a> ·
<a href="docs/contributing/README.md">贡献与 CI</a>
</p>
> 本文是对 `README.md` 的人工对齐翻译(强调可读性与准确性,不做逐字直译)。
>
> 技术标识命令、配置键、API 路径、Trait 名称)保持英文,避免语义漂移。
>
> 最后对齐时间:**2026-02-22**。
## 📢 公告板
用于发布重要通知(破坏性变更、安全通告、维护窗口、版本阻塞问题等)。
| 日期UTC | 级别 | 通知 | 处理建议 |
|---|---|---|---|
| 2026-02-19 | _紧急_ | 我们与 `openagen/zeroclaw``zeroclaw.org` **没有任何关系**。`zeroclaw.org` 当前会指向 `openagen/zeroclaw` 这个 fork并且该域名/仓库正在冒充我们的官网与官方项目。 | 请不要相信上述来源发布的任何信息、二进制、募资活动或官方声明。请仅以[本仓库](https://github.com/zeroclaw-labs/zeroclaw)和已验证官方社媒为准。 |
| 2026-02-21 | _重要_ | 我们的官网现已上线:[zeroclawlabs.ai](https://zeroclawlabs.ai)。感谢大家一直以来的耐心等待。我们仍在持续发现冒充行为,请勿参与任何未经我们官方渠道发布、但打着 ZeroClaw 名义进行的投资、募资或类似活动。 | 一切信息请以[本仓库](https://github.com/zeroclaw-labs/zeroclaw)为准;也可关注 [X@zeroclawlabs](https://x.com/zeroclawlabs?s=21)、[Telegram@zeroclawlabs](https://t.me/zeroclawlabs)、[Facebook群组](https://www.facebook.com/groups/zeroclaw)、[Redditr/zeroclawlabs](https://www.reddit.com/r/zeroclawlabs/) 与 [小红书账号](https://www.xiaohongshu.com/user/profile/67cbfc43000000000d008307?xsec_token=AB73VnYnGNx5y36EtnnZfGmAmS-6Wzv8WMuGpfwfkg6Yc%3D&xsec_source=pc_search) 获取官方最新动态。 |
| 2026-02-19 | _重要_ | Anthropic 于 2026-02-19 更新了 Authentication and Credential Use 条款。条款明确OAuth authentication用于 Free、Pro、Max仅适用于 Claude Code 与 Claude.ai将 Claude Free/Pro/Max 账号获得的 OAuth token 用于其他任何产品、工具或服务(包括 Agent SDK不被允许并可能构成对 Consumer Terms of Service 的违规。 | 为避免损失,请暂时不要尝试 Claude Code OAuth 集成;原文见:[Authentication and Credential Use](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use)。 |
## 项目简介
ZeroClaw 是一个高性能、低资源占用、可组合的自主智能体运行时。ZeroClaw 是面向智能代理工作流的**运行时操作系统** — 它抽象了模型、工具、记忆和执行层,使代理可以一次构建、随处运行。
- Rust 原生实现,单二进制部署,跨 ARM / x86 / RISC-V。
- Trait 驱动架构,`Provider` / `Channel` / `Tool` / `Memory` 可替换。
- 安全默认值优先:配对鉴权、显式 allowlist、沙箱与作用域约束。
## 为什么选择 ZeroClaw
- **默认轻量运行时**:常见 CLI 与 `status` 工作流通常保持在几 MB 级内存范围。
- **低成本部署友好**:面向低价板卡与小规格云主机设计,不依赖厚重运行时。
- **冷启动速度快**Rust 单二进制让常用命令与守护进程启动更接近“秒开”。
- **跨架构可移植**:同一套二进制优先流程覆盖 ARM / x86 / RISC-V并保持 provider/channel/tool 可替换。
## 基准快照ZeroClaw vs OpenClaw可复现
以下是本地快速基准对比macOS arm642026 年 2 月),按 0.8GHz 边缘 CPU 进行归一化展示:
| | OpenClaw | NanoBot | PicoClaw | ZeroClaw 🦀 |
|---|---|---|---|---|
| **语言** | TypeScript | Python | Go | **Rust** |
| **RAM** | > 1GB | > 100MB | < 10MB | **< 5MB** |
| **启动时间0.8GHz 核)** | > 500s | > 30s | < 1s | **< 10ms** |
| **二进制体积** | ~28MBdist | N/A脚本 | ~8MB | **~8.8 MB** |
| **成本** | Mac Mini $599 | Linux SBC ~$50 | Linux 板卡 $10 | **任意 $10 硬件** |
> 说明ZeroClaw 的数据来自 release 构建,并通过 `/usr/bin/time -l` 测得。OpenClaw 需要 Node.js 运行时环境,仅该运行时通常就会带来约 390MB 的额外内存占用NanoBot 需要 Python 运行时环境。PicoClaw 与 ZeroClaw 为静态二进制。
<p align="center">
<img src="zero-claw.jpeg" alt="ZeroClaw vs OpenClaw 对比图" width="800" />
</p>
### 本地可复现测量
基准数据会随代码与工具链变化,建议始终在你的目标环境自行复测:
```bash
cargo build --release
ls -lh target/release/zeroclaw
/usr/bin/time -l target/release/zeroclaw --help
/usr/bin/time -l target/release/zeroclaw status
```
当前 README 的样例数据macOS arm642026-02-18
- Release 二进制:`8.8M`
- `zeroclaw --help`:约 `0.02s`,峰值内存约 `3.9MB`
- `zeroclaw status`:约 `0.01s`,峰值内存约 `4.1MB`
## 一键部署
```bash
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
./bootstrap.sh
```
可选环境初始化:`./bootstrap.sh --install-system-deps --install-rust`(可能需要 `sudo`)。
详细说明见:[`docs/one-click-bootstrap.md`](docs/one-click-bootstrap.md)。
## 快速开始
### HomebrewmacOS/Linuxbrew
```bash
brew install zeroclaw
```
```bash
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
cargo build --release --locked
cargo install --path . --force --locked
# 快速初始化(无交互)
zeroclaw onboard --api-key sk-... --provider openrouter
# 或使用交互式向导
zeroclaw onboard --interactive
# 单次对话
zeroclaw agent -m "Hello, ZeroClaw!"
# 启动网关(默认: 127.0.0.1:42617
zeroclaw gateway
# 启动长期运行模式
zeroclaw daemon
```
## Subscription AuthOpenAI Codex / Claude Code
ZeroClaw 现已支持基于订阅的原生鉴权配置(多账号、静态加密存储)。
- 配置文件:`~/.zeroclaw/auth-profiles.json`
- 加密密钥:`~/.zeroclaw/.secret_key`
- Profile ID 格式:`<provider>:<profile_name>`(例:`openai-codex:work`
OpenAI Codex OAuthChatGPT 订阅):
```bash
# 推荐用于服务器/无显示器环境
zeroclaw auth login --provider openai-codex --device-code
# 浏览器/回调流程,支持粘贴回退
zeroclaw auth login --provider openai-codex --profile default
zeroclaw auth paste-redirect --provider openai-codex --profile default
# 检查 / 刷新 / 切换 profile
zeroclaw auth status
zeroclaw auth refresh --provider openai-codex --profile default
zeroclaw auth use --provider openai-codex --profile work
```
Claude Code / Anthropic setup-token
```bash
# 粘贴订阅/setup tokenAuthorization header 模式)
zeroclaw auth paste-token --provider anthropic --profile default --auth-kind authorization
# 别名命令
zeroclaw auth setup-token --provider anthropic --profile default
```
使用 subscription auth 运行 agent
```bash
zeroclaw agent --provider openai-codex -m "hello"
zeroclaw agent --provider openai-codex --auth-profile openai-codex:work -m "hello"
# Anthropic 同时支持 API key 和 auth token 环境变量:
# ANTHROPIC_AUTH_TOKEN, ANTHROPIC_OAUTH_TOKEN, ANTHROPIC_API_KEY
zeroclaw agent --provider anthropic -m "hello"
```
## 架构
每个子系统都是一个 **Trait** — 通过配置切换即可更换实现,无需修改代码。
<p align="center">
<img src="docs/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/commands-reference.md`](docs/commands-reference.md)
- 配置参考:[`docs/config-reference.md`](docs/config-reference.md)
- Provider 参考:[`docs/providers-reference.md`](docs/providers-reference.md)
- Channel 参考:[`docs/channels-reference.md`](docs/channels-reference.md)
- 运维手册:[`docs/operations-runbook.md`](docs/operations-runbook.md)
- 故障排查:[`docs/troubleshooting.md`](docs/troubleshooting.md)
- 文档清单与分类:[`docs/docs-inventory.md`](docs/docs-inventory.md)
- 项目 triage 快照2026-02-18[`docs/project-triage-snapshot-2026-02-18.md`](docs/project-triage-snapshot-2026-02-18.md)
## 贡献与许可证
- 贡献指南:[`CONTRIBUTING.md`](CONTRIBUTING.md)
- PR 工作流:[`docs/pr-workflow.md`](docs/pr-workflow.md)
- Reviewer 指南:[`docs/reviewer-playbook.md`](docs/reviewer-playbook.md)
- 许可证MIT 或 Apache 2.0(见 [`LICENSE-MIT`](LICENSE-MIT)、[`LICENSE-APACHE`](LICENSE-APACHE) 与 [`NOTICE`](NOTICE)
---
如果你需要完整实现细节(架构图、全部命令、完整 API、开发流程请直接阅读英文主文档[`README.md`](README.md)。

View File

@ -13,6 +13,8 @@
cargo test telegram --lib
```
Toolchain note: CI/release metadata is aligned with Rust `1.88`; use the same stable toolchain when reproducing release-facing checks locally.
## 📝 What Was Created For You
### 1. **test_telegram_integration.sh** (Main Test Suite)
@ -298,6 +300,6 @@ If all tests pass:
## 📞 Support
- Issues: https://github.com/theonlyhennygod/zeroclaw/issues
- Issues: https://github.com/zeroclaw-labs/zeroclaw/issues
- Docs: `./TESTING_TELEGRAM.md`
- Help: `zeroclaw --help`

View File

@ -32,6 +32,20 @@ Preferred reporting paths:
- Suggested mitigation or patch direction (if known)
- Any known workaround
## Official Channels and Anti-Fraud Notice
Impersonation scams are a real risk in open communities.
Security-critical rule:
- ZeroClaw maintainers will not ask for cryptocurrency, wallet seed phrases, or private financial credentials.
- Treat direct-message payment requests as fraudulent unless independently verified in the repository.
- Verify announcements using repository sources first.
Canonical statement and reporting guidance:
- [docs/security/official-channels-and-fraud-prevention.md](docs/security/official-channels-and-fraud-prevention.md)
## Maintainer Handling Workflow (GitHub-Native)
### 1. Intake and triage (private)

View File

@ -115,6 +115,9 @@ After running automated tests, perform these manual checks:
- Send message with @botname mention
- Verify: Bot responds and mention is stripped
- DM/private chat should always work regardless of mention_only
- Regression check (group non-text): verify group media without mention does not trigger bot reply
- Regression command:
`cargo test -q telegram_mention_only_group_photo_without_caption_is_ignored`
6. **Error logging**
@ -297,7 +300,7 @@ on: [push, pull_request]
jobs:
test:
runs-on: [self-hosted, Linux, X64]
runs-on: [self-hosted, aws-india]
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
@ -349,4 +352,4 @@ zeroclaw channel doctor
- [Telegram Bot API Documentation](https://core.telegram.org/bots/api)
- [ZeroClaw Main README](README.md)
- [Contributing Guide](CONTRIBUTING.md)
- [Issue Tracker](https://github.com/theonlyhennygod/zeroclaw/issues)
- [Issue Tracker](https://github.com/zeroclaw-labs/zeroclaw/issues)

View File

@ -41,6 +41,9 @@ impl BenchProvider {
tool_calls: vec![],
usage: None,
reasoning_content: None,
quota_metadata: None,
stop_reason: None,
raw_stop_reason: None,
}]),
}
}
@ -57,12 +60,18 @@ impl BenchProvider {
}],
usage: None,
reasoning_content: None,
quota_metadata: None,
stop_reason: None,
raw_stop_reason: None,
},
ChatResponse {
text: Some("done".into()),
tool_calls: vec![],
usage: None,
reasoning_content: None,
quota_metadata: None,
stop_reason: None,
raw_stop_reason: None,
},
]),
}
@ -94,6 +103,9 @@ impl Provider for BenchProvider {
tool_calls: vec![],
usage: None,
reasoning_content: None,
quota_metadata: None,
stop_reason: None,
raw_stop_reason: None,
});
}
Ok(guard.remove(0))
@ -161,6 +173,9 @@ Let me know if you need more."#
tool_calls: vec![],
usage: None,
reasoning_content: None,
quota_metadata: None,
stop_reason: None,
raw_stop_reason: None,
};
let multi_tool = ChatResponse {
@ -179,6 +194,9 @@ Let me know if you need more."#
tool_calls: vec![],
usage: None,
reasoning_content: None,
quota_metadata: None,
stop_reason: None,
raw_stop_reason: None,
};
c.bench_function("xml_parse_single_tool_call", |b| {
@ -213,6 +231,9 @@ fn bench_native_parsing(c: &mut Criterion) {
],
usage: None,
reasoning_content: None,
quota_metadata: None,
stop_reason: None,
raw_stop_reason: None,
};
c.bench_function("native_parse_tool_calls", |b| {

19
build_and_run.sh Normal file
View File

@ -0,0 +1,19 @@
#!/bin/bash
# Build ZeroClaw in release mode
echo "Building ZeroClaw in release mode..."
cargo build --release
# Check if build was successful
if [ $? -eq 0 ]; then
echo "Build successful!"
echo "To start the web dashboard, run:"
echo "./target/release/zeroclaw gateway"
echo ""
echo "The dashboard will typically be available at http://127.0.0.1:3000/"
echo "You can also specify a custom port with -p, e.g.:"
echo "./target/release/zeroclaw gateway -p 8080"
else
echo "Build failed!"
exit 1
fi

8
build_release.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
# Build ZeroClaw in release mode
set -e
echo "Building ZeroClaw in release mode..."
cd /Users/argenisdelarosa/Downloads/zeroclaw
cargo build --release
echo "Build completed successfully!"
echo "Binary location: target/release/zeroclaw"

View File

@ -0,0 +1,43 @@
[package]
name = "zeroclaw-android-bridge"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Android JNI bridge for ZeroClaw"
[lib]
crate-type = ["cdylib"]
name = "zeroclaw_android"
[dependencies]
# Note: zeroclaw dep commented out until we integrate properly
# zeroclaw = { path = "../.." }
uniffi = { version = "0.27" }
# Minimal tokio - only what we need
tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "sync"] }
anyhow = "1"
serde = { version = "1", default-features = false, features = ["derive"] }
serde_json = "1"
# Minimal tracing for mobile
tracing = { version = "0.1", default-features = false }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter"] }
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
# ============================================
# BINARY SIZE OPTIMIZATION
# ============================================
# Target: <3MB native library per ABI
[profile.release]
# Optimize for size over speed
opt-level = "z" # Smallest binary (was "3" for speed)
lto = true # Link-time optimization - removes dead code
codegen-units = 1 # Better optimization, slower compile
panic = "abort" # No unwinding = smaller binary
strip = true # Strip symbols
[profile.release.package."*"]
opt-level = "z" # Apply to all dependencies too

View File

@ -0,0 +1,305 @@
#![forbid(unsafe_code)]
//! ZeroClaw Android Bridge
//!
//! This crate provides UniFFI bindings for ZeroClaw to be used from Kotlin/Android.
//! It exposes a simplified API for:
//! - Starting/stopping the gateway
//! - Sending messages to the agent
//! - Receiving responses
//! - Managing configuration
use std::sync::{Arc, Mutex, OnceLock};
use tokio::runtime::Runtime;
uniffi::setup_scaffolding!();
/// Global runtime for async operations
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
fn runtime() -> &'static Runtime {
RUNTIME.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("Failed to create Tokio runtime")
})
}
/// Agent status enum exposed to Kotlin
#[derive(Debug, Clone, uniffi::Enum)]
pub enum AgentStatus {
Stopped,
Starting,
Running,
Thinking,
Error { message: String },
}
/// Configuration for the ZeroClaw agent
#[derive(Debug, Clone, uniffi::Record)]
pub struct ZeroClawConfig {
pub data_dir: String,
pub provider: String,
pub model: String,
pub api_key: String,
pub system_prompt: Option<String>,
}
impl Default for ZeroClawConfig {
fn default() -> Self {
Self {
data_dir: String::new(),
provider: "anthropic".to_string(),
model: "claude-sonnet-4-5".to_string(),
api_key: String::new(),
system_prompt: None,
}
}
}
/// A message in the conversation
#[derive(Debug, Clone, uniffi::Record)]
pub struct ChatMessage {
pub id: String,
pub content: String,
pub role: String, // "user" | "assistant" | "system"
pub timestamp_ms: i64,
}
/// Response from sending a message
#[derive(Debug, Clone, uniffi::Record)]
pub struct SendResult {
pub success: bool,
pub message_id: Option<String>,
pub error: Option<String>,
}
/// Main ZeroClaw controller exposed to Android
#[derive(uniffi::Object)]
pub struct ZeroClawController {
config: Mutex<ZeroClawConfig>,
status: Mutex<AgentStatus>,
messages: Mutex<Vec<ChatMessage>>,
// TODO: Add actual gateway handle
// gateway: Mutex<Option<GatewayHandle>>,
}
#[uniffi::export]
impl ZeroClawController {
/// Create a new controller with the given config
#[uniffi::constructor]
pub fn new(config: ZeroClawConfig) -> Arc<Self> {
// Initialize logging
let _ = tracing_subscriber::fmt()
.with_env_filter("zeroclaw=info")
.try_init();
Arc::new(Self {
config: Mutex::new(config),
status: Mutex::new(AgentStatus::Stopped),
messages: Mutex::new(Vec::new()),
})
}
/// Create with default config
#[uniffi::constructor]
pub fn with_defaults(data_dir: String) -> Arc<Self> {
let mut config = ZeroClawConfig::default();
config.data_dir = data_dir;
Self::new(config)
}
/// Start the ZeroClaw gateway
pub fn start(&self) -> Result<(), ZeroClawError> {
let mut status = self.status.lock().map_err(|_| ZeroClawError::LockError)?;
if matches!(*status, AgentStatus::Running | AgentStatus::Starting) {
return Ok(());
}
*status = AgentStatus::Starting;
drop(status);
// TODO: Actually start the gateway
// runtime().spawn(async move {
// let config = zeroclaw::Config::load()?;
// let gateway = zeroclaw::Gateway::new(config).await?;
// gateway.run().await
// });
// For now, simulate successful start
let mut status = self.status.lock().map_err(|_| ZeroClawError::LockError)?;
*status = AgentStatus::Running;
tracing::info!("ZeroClaw gateway started");
Ok(())
}
/// Stop the gateway
pub fn stop(&self) -> Result<(), ZeroClawError> {
let mut status = self.status.lock().map_err(|_| ZeroClawError::LockError)?;
// TODO: Actually stop the gateway
// if let Some(gateway) = self.gateway.lock()?.take() {
// gateway.shutdown();
// }
*status = AgentStatus::Stopped;
tracing::info!("ZeroClaw gateway stopped");
Ok(())
}
/// Get current agent status
pub fn get_status(&self) -> AgentStatus {
self.status
.lock()
.map(|s| s.clone())
.unwrap_or(AgentStatus::Error {
message: "Failed to get status".to_string(),
})
}
/// Send a message to the agent
pub fn send_message(&self, content: String) -> SendResult {
let msg_id = uuid_v4();
// Add user message
if let Ok(mut messages) = self.messages.lock() {
messages.push(ChatMessage {
id: msg_id.clone(),
content: content.clone(),
role: "user".to_string(),
timestamp_ms: current_timestamp_ms(),
});
}
// TODO: Actually send to gateway and get response
// For now, echo back
if let Ok(mut messages) = self.messages.lock() {
messages.push(ChatMessage {
id: uuid_v4(),
content: format!("Echo: {}", content),
role: "assistant".to_string(),
timestamp_ms: current_timestamp_ms(),
});
}
SendResult {
success: true,
message_id: Some(msg_id),
error: None,
}
}
/// Get conversation history
pub fn get_messages(&self) -> Vec<ChatMessage> {
self.messages
.lock()
.map(|m| m.clone())
.unwrap_or_default()
}
/// Clear conversation history
pub fn clear_messages(&self) {
if let Ok(mut messages) = self.messages.lock() {
messages.clear();
}
}
/// Update configuration
pub fn update_config(&self, config: ZeroClawConfig) -> Result<(), ZeroClawError> {
let mut current = self.config.lock().map_err(|_| ZeroClawError::LockError)?;
*current = config;
Ok(())
}
/// Get current configuration
pub fn get_config(&self) -> Result<ZeroClawConfig, ZeroClawError> {
self.config
.lock()
.map(|c| c.clone())
.map_err(|_| ZeroClawError::LockError)
}
/// Check if API key is configured
pub fn is_configured(&self) -> bool {
self.config
.lock()
.map(|c| !c.api_key.is_empty())
.unwrap_or(false)
}
}
/// Errors that can occur in the bridge
#[derive(Debug, Clone, uniffi::Error)]
pub enum ZeroClawError {
NotInitialized,
AlreadyRunning,
ConfigError { message: String },
GatewayError { message: String },
LockError,
}
impl std::fmt::Display for ZeroClawError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotInitialized => write!(f, "ZeroClaw not initialized"),
Self::AlreadyRunning => write!(f, "Gateway already running"),
Self::ConfigError { message } => write!(f, "Config error: {}", message),
Self::GatewayError { message } => write!(f, "Gateway error: {}", message),
Self::LockError => write!(f, "Failed to acquire lock"),
}
}
}
impl std::error::Error for ZeroClawError {}
// Helper functions
fn uuid_v4() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{:x}", now)
}
fn current_timestamp_ms() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_controller_creation() {
let controller = ZeroClawController::with_defaults("/tmp/zeroclaw".to_string());
assert!(matches!(controller.get_status(), AgentStatus::Stopped));
}
#[test]
fn test_start_stop() {
let controller = ZeroClawController::with_defaults("/tmp/zeroclaw".to_string());
controller.start().unwrap();
assert!(matches!(controller.get_status(), AgentStatus::Running));
controller.stop().unwrap();
assert!(matches!(controller.get_status(), AgentStatus::Stopped));
}
#[test]
fn test_send_message() {
let controller = ZeroClawController::with_defaults("/tmp/zeroclaw".to_string());
let result = controller.send_message("Hello".to_string());
assert!(result.success);
let messages = controller.get_messages();
assert_eq!(messages.len(), 2); // User + assistant
}
}

View File

@ -0,0 +1,5 @@
#![forbid(unsafe_code)]
fn main() {
uniffi::uniffi_bindgen_main()
}

108
clients/android/README.md Normal file
View File

@ -0,0 +1,108 @@
# ZeroClaw Android Client 🦀📱
Native Android client for ZeroClaw - run your autonomous AI assistant on Android.
## Features
- 🚀 **Native Performance** - Kotlin/Jetpack Compose, not a webview
- 🔋 **Battery Efficient** - WorkManager, Doze-aware, minimal wake locks
- 🔐 **Security First** - Android Keystore for secrets, sandboxed execution
- 🦀 **ZeroClaw Core** - Full Rust binary via UniFFI/JNI
- 🎨 **Material You** - Dynamic theming, modern Android UX
## Requirements
- Android 8.0+ (API 26+)
- ~50MB storage
- ARM64 (arm64-v8a) or ARMv7 (armeabi-v7a)
## Building
### Prerequisites
```bash
# Install Rust Android targets
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
# Install cargo-ndk
cargo install cargo-ndk
# Android SDK (via Android Studio or sdkman)
# NDK r25+ required
```
### Build APK
```bash
cd clients/android
./gradlew assembleDebug
```
### Build with Rust
```bash
# Build native library first
cargo ndk -t arm64-v8a -o app/src/main/jniLibs build --release
# Then build APK
./gradlew assembleRelease
```
## Architecture
```
┌─────────────────────────────────────┐
│ UI (Jetpack Compose) │
├─────────────────────────────────────┤
│ Service Layer (Kotlin) │
│ ├─ ZeroClawService │
│ ├─ NotificationHandler │
│ └─ WorkManager Jobs │
├─────────────────────────────────────┤
│ Bridge (UniFFI) │
├─────────────────────────────────────┤
│ Native (libzeroclaw.so) │
└─────────────────────────────────────┘
```
## Status
**Phase 1: Foundation** (Complete)
- [x] Project setup (Kotlin/Compose/Gradle)
- [x] Basic JNI bridge stub
- [x] Foreground service
- [x] Notification channels
- [x] Boot receiver
**Phase 2: Core Features** (Complete)
- [x] UniFFI bridge crate
- [x] Settings UI (provider/model/API key)
- [x] Chat UI scaffold
- [x] Theme system (Material 3)
**Phase 3: Integration** (Complete)
- [x] WorkManager for cron/heartbeat
- [x] DataStore + encrypted preferences
- [x] Quick Settings tile
- [x] Share intent handling
- [x] Battery optimization helpers
- [x] CI workflow for Android builds
**Phase 4: Polish** (Complete)
- [x] Home screen widget
- [x] Accessibility utilities (TalkBack support)
- [x] One-liner install scripts (Termux, ADB)
- [x] Web installer page
🚀 **Ready for Production**
- [ ] Cargo NDK CI integration
- [ ] F-Droid submission
- [ ] Google Play submission
## Contributing
See the RFC in issue discussions for design decisions.
## License
Same as ZeroClaw (MIT/Apache-2.0)

97
clients/android/SIZE.md Normal file
View File

@ -0,0 +1,97 @@
# ZeroClaw Android - Binary Size Optimization
## Target Sizes
| Component | Target | Notes |
|-----------|--------|-------|
| Native lib (per ABI) | <3MB | Rust, optimized for size |
| APK (arm64-v8a) | <10MB | Single ABI, most users |
| APK (universal) | <20MB | All ABIs, fallback |
## Optimization Strategy
### 1. Rust Native Library
```toml
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
panic = "abort" # No unwinding overhead
strip = true # Remove symbols
```
**Expected savings:** ~40% reduction vs default release
### 2. Android APK
**Enabled:**
- R8 minification (`isMinifyEnabled = true`)
- Resource shrinking (`isShrinkResources = true`)
- ABI splits (users download only their arch)
- Aggressive ProGuard rules
**Removed:**
- `material-icons-extended` (~5MB → 0MB)
- `kotlinx-serialization` (~300KB, unused)
- `ui-tooling-preview` (~100KB, debug only)
- Debug symbols in release
### 3. Dependencies Audit
| Dependency | Size | Keep? |
|------------|------|-------|
| Compose BOM | ~3MB | ✅ Required |
| Material3 | ~1MB | ✅ Required |
| material-icons-extended | ~5MB | ❌ Removed |
| Navigation | ~200KB | ✅ Required |
| DataStore | ~100KB | ✅ Required |
| WorkManager | ~300KB | ✅ Required |
| Security-crypto | ~100KB | ✅ Required |
| Coroutines | ~200KB | ✅ Required |
| Serialization | ~300KB | ❌ Removed (unused) |
### 4. Split APKs
```kotlin
splits {
abi {
isEnable = true
include("arm64-v8a", "armeabi-v7a", "x86_64")
isUniversalApk = true
}
}
```
**Result:**
- `app-arm64-v8a-release.apk` → ~10MB (90% of users)
- `app-armeabi-v7a-release.apk` → ~9MB (older devices)
- `app-x86_64-release.apk` → ~10MB (emulators)
- `app-universal-release.apk` → ~18MB (fallback)
## Measuring Size
```bash
# Build release APK
./gradlew assembleRelease
# Check sizes
ls -lh app/build/outputs/apk/release/
# Analyze APK contents
$ANDROID_HOME/build-tools/34.0.0/apkanalyzer apk summary app-release.apk
```
## Future Optimizations
1. **Baseline Profiles** - Pre-compile hot paths
2. **R8 full mode** - More aggressive shrinking
3. **Custom Compose compiler** - Smaller runtime
4. **WebP images** - Smaller than PNG
5. **Dynamic delivery** - On-demand features
## Philosophy
> "Zero overhead. Zero compromise."
Every KB matters. We ship what users need, nothing more.

View File

@ -0,0 +1,140 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "ai.zeroclaw.android"
compileSdk = 34
defaultConfig {
applicationId = "ai.zeroclaw.android"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Aggressive optimization
ndk {
debugSymbolLevel = "NONE"
}
}
debug {
isDebuggable = true
applicationIdSuffix = ".debug"
}
}
// Split APKs by ABI - users only download what they need
splits {
abi {
isEnable = true
reset()
include("arm64-v8a", "armeabi-v7a", "x86_64")
isUniversalApk = true // Also build universal for fallback
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
// Task to build native library before APK
tasks.register("buildRustLibrary") {
doLast {
exec {
workingDir = rootProject.projectDir.parentFile.parentFile // zeroclaw root
commandLine("cargo", "ndk",
"-t", "arm64-v8a",
"-t", "armeabi-v7a",
"-t", "x86_64",
"-o", "clients/android/app/src/main/jniLibs",
"build", "--release", "-p", "zeroclaw-android-bridge")
}
}
}
}
dependencies {
// Core Android
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
// Compose - minimal set
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.material3:material3")
// NOTE: Using material-icons-core (small) instead of extended (5MB+)
// Add individual icons via drawable if needed
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.7")
// DataStore (preferences)
implementation("androidx.datastore:datastore-preferences:1.0.0")
// WorkManager (background tasks)
implementation("androidx.work:work-runtime-ktx:2.9.0")
// Security (Keystore)
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// NOTE: Serialization removed - not used yet, saves ~300KB
// Add back when needed: implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
// Testing
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
// Debug
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}

67
clients/android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,67 @@
# ZeroClaw Android ProGuard Rules
# Goal: Smallest possible APK
# ============================================
# KEEP NATIVE BRIDGE
# ============================================
-keep class ai.zeroclaw.android.bridge.** { *; }
-keepclassmembers class ai.zeroclaw.android.bridge.** { *; }
# Keep JNI methods
-keepclasseswithmembernames class * {
native <methods>;
}
# ============================================
# KEEP DATA CLASSES
# ============================================
-keep class ai.zeroclaw.android.data.** { *; }
-keepclassmembers class ai.zeroclaw.android.data.** { *; }
# ============================================
# KOTLIN SERIALIZATION
# ============================================
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
# ============================================
# AGGRESSIVE OPTIMIZATIONS
# ============================================
# Remove logging in release
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
public static int i(...);
}
# KEEP Kotlin null checks - stripping them hides bugs and causes crashes
# (Previously removed; CodeRabbit HIGH severity fix)
# -assumenosideeffects class kotlin.jvm.internal.Intrinsics { ... }
# Optimize enums
-optimizations !code/simplification/enum*
# Remove unused Compose stuff
-dontwarn androidx.compose.**
# ============================================
# SIZE OPTIMIZATIONS
# ============================================
# Merge classes where possible
-repackageclasses ''
-allowaccessmodification
# Remove unused code paths
-optimizationpasses 5
# Don't keep attributes we don't need
-keepattributes SourceFile,LineNumberTable # Keep for crash reports
-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Network -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Background execution -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Battery optimization (optional - for requesting exemption) -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application
android:name=".ZeroClawApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ZeroClaw"
tools:targetApi="34">
<!-- Main Activity -->
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.ZeroClaw"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Handle text share intents -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<!-- Handle URL share intents -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/uri-list" />
</intent-filter>
<!-- Handle image share intents -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<!-- Background Service -->
<service
android:name=".service.ZeroClawService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<!-- Quick Settings Tile -->
<service
android:name=".tile.ZeroClawTileService"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/app_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
</service>
<!-- Boot Receiver -->
<receiver
android:name=".receiver.BootReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<!-- Home Screen Widget -->
<receiver
android:name=".widget.ZeroClawWidget"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="ai.zeroclaw.widget.TOGGLE" />
<action android:name="ai.zeroclaw.widget.QUICK_MESSAGE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<!-- WorkManager Initialization (disable default, we initialize manually) -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>

View File

@ -0,0 +1,212 @@
package ai.zeroclaw.android
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import ai.zeroclaw.android.ui.theme.ZeroClawTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ZeroClawTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ZeroClawApp()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ZeroClawApp() {
var agentStatus by remember { mutableStateOf(AgentStatus.Stopped) }
var messages by remember { mutableStateOf(listOf<ChatMessage>()) }
var inputText by remember { mutableStateOf("") }
Scaffold(
topBar = {
TopAppBar(
title = { Text("ZeroClaw") },
actions = {
StatusIndicator(status = agentStatus)
}
)
},
bottomBar = {
ChatInput(
text = inputText,
onTextChange = { inputText = it },
onSend = {
if (inputText.isNotBlank()) {
messages = messages + ChatMessage(
content = inputText,
isUser = true
)
inputText = ""
// TODO: Send to native layer
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
if (messages.isEmpty()) {
EmptyState(
status = agentStatus,
onStart = { agentStatus = AgentStatus.Running }
)
} else {
ChatMessageList(
messages = messages,
modifier = Modifier.weight(1f)
)
}
}
}
}
@Composable
fun StatusIndicator(status: AgentStatus) {
val (color, text) = when (status) {
AgentStatus.Running -> MaterialTheme.colorScheme.primary to "Running"
AgentStatus.Stopped -> MaterialTheme.colorScheme.outline to "Stopped"
AgentStatus.Error -> MaterialTheme.colorScheme.error to "Error"
}
Surface(
color = color.copy(alpha = 0.2f),
shape = MaterialTheme.shapes.small
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
color = color,
style = MaterialTheme.typography.labelMedium
)
}
}
@Composable
fun EmptyState(status: AgentStatus, onStart: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "🦀",
style = MaterialTheme.typography.displayLarge
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "ZeroClaw",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Your AI assistant, running locally",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
if (status == AgentStatus.Stopped) {
Button(onClick = onStart) {
Text("Start Agent")
}
}
}
}
@Composable
fun ChatInput(
text: String,
onTextChange: (String) -> Unit,
onSend: () -> Unit
) {
Surface(
tonalElevation = 3.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = text,
onValueChange = onTextChange,
modifier = Modifier.weight(1f),
placeholder = { Text("Message ZeroClaw...") },
singleLine = true
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(onClick = onSend) {
Text("")
}
}
}
}
@Composable
fun ChatMessageList(messages: List<ChatMessage>, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
messages.forEach { message ->
ChatBubble(message = message)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
fun ChatBubble(message: ChatMessage) {
val alignment = if (message.isUser) Alignment.End else Alignment.Start
val color = if (message.isUser)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surfaceVariant
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = if (message.isUser) Alignment.CenterEnd else Alignment.CenterStart
) {
Surface(
color = color,
shape = MaterialTheme.shapes.medium
) {
Text(
text = message.content,
modifier = Modifier.padding(12.dp)
)
}
}
}
data class ChatMessage(
val content: String,
val isUser: Boolean,
val timestamp: Long = System.currentTimeMillis()
)
enum class AgentStatus {
Running, Stopped, Error
}

View File

@ -0,0 +1,104 @@
package ai.zeroclaw.android
import android.content.Intent
import android.net.Uri
/**
* Handles content shared TO ZeroClaw from other apps.
*
* Supports:
* - Plain text
* - URLs
* - Images (future)
* - Files (future)
*/
object ShareHandler {
sealed class SharedContent {
data class Text(val text: String) : SharedContent()
data class Url(val url: String, val title: String? = null) : SharedContent()
data class Image(val uri: Uri) : SharedContent()
data class File(val uri: Uri, val mimeType: String) : SharedContent()
object None : SharedContent()
}
/**
* Parse incoming share intent
*/
fun parseIntent(intent: Intent): SharedContent {
if (intent.action != Intent.ACTION_SEND) {
return SharedContent.None
}
val type = intent.type ?: return SharedContent.None
return when {
type == "text/plain" -> parseTextIntent(intent)
type == "text/uri-list" -> parseUriListIntent(intent)
type.startsWith("image/") -> parseImageIntent(intent)
else -> parseFileIntent(intent, type)
}
}
private fun parseTextIntent(intent: Intent): SharedContent {
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return SharedContent.None
// Check if it's a URL
if (text.startsWith("http://") || text.startsWith("https://")) {
val title = intent.getStringExtra(Intent.EXTRA_SUBJECT)
return SharedContent.Url(text, title)
}
return SharedContent.Text(text)
}
private fun parseUriListIntent(intent: Intent): SharedContent {
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return SharedContent.None
// text/uri-list contains URLs separated by newlines
val firstUrl = text.lines().firstOrNull { it.startsWith("http://") || it.startsWith("https://") }
return if (firstUrl != null) {
val title = intent.getStringExtra(Intent.EXTRA_SUBJECT)
SharedContent.Url(firstUrl, title)
} else {
SharedContent.Text(text)
}
}
private fun parseImageIntent(intent: Intent): SharedContent {
val uri = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
return uri?.let { SharedContent.Image(it) } ?: SharedContent.None
}
private fun parseFileIntent(intent: Intent, mimeType: String): SharedContent {
val uri = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
return uri?.let { SharedContent.File(it, mimeType) } ?: SharedContent.None
}
/**
* Generate a prompt from shared content
*/
fun generatePrompt(content: SharedContent): String {
return when (content) {
is SharedContent.Text -> "I'm sharing this text with you:\n\n${content.text}"
is SharedContent.Url -> {
val title = content.title?.let { "\"$it\"\n" } ?: ""
"${title}I'm sharing this URL: ${content.url}\n\nPlease summarize or help me with this."
}
is SharedContent.Image -> "I'm sharing an image with you. [Image attached]"
is SharedContent.File -> "I'm sharing a file with you. [File: ${content.mimeType}]"
SharedContent.None -> ""
}
}
}

View File

@ -0,0 +1,116 @@
package ai.zeroclaw.android
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.work.Configuration
import androidx.work.WorkManager
import ai.zeroclaw.android.data.SettingsRepository
import ai.zeroclaw.android.worker.HeartbeatWorker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class ZeroClawApp : Application(), Configuration.Provider {
companion object {
const val CHANNEL_ID = "zeroclaw_service"
const val CHANNEL_NAME = "ZeroClaw Agent"
const val AGENT_CHANNEL_ID = "zeroclaw_agent"
const val AGENT_CHANNEL_NAME = "Agent Messages"
// Singleton instance for easy access
lateinit var instance: ZeroClawApp
private set
}
// Application scope for coroutines
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
// Lazy initialized repositories
val settingsRepository by lazy { SettingsRepository(this) }
override fun onCreate() {
super.onCreate()
instance = this
createNotificationChannels()
initializeWorkManager()
// Schedule heartbeat if auto-start is enabled
applicationScope.launch {
val settings = settingsRepository.settings.first()
if (settings.autoStart && settings.isConfigured()) {
HeartbeatWorker.scheduleHeartbeat(
this@ZeroClawApp,
settings.heartbeatIntervalMinutes.toLong()
)
}
}
// Listen for settings changes and update heartbeat schedule
applicationScope.launch {
settingsRepository.settings
.map { Triple(it.autoStart, it.isConfigured(), it.heartbeatIntervalMinutes) }
.distinctUntilChanged()
.collect { (autoStart, isConfigured, intervalMinutes) ->
if (autoStart && isConfigured) {
HeartbeatWorker.scheduleHeartbeat(this@ZeroClawApp, intervalMinutes.toLong())
} else {
HeartbeatWorker.cancelHeartbeat(this@ZeroClawApp)
}
}
}
// TODO: Initialize native library
// System.loadLibrary("zeroclaw_android")
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
// Service channel (foreground service - low priority, silent)
val serviceChannel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
description = "ZeroClaw background service notification"
setShowBadge(false)
enableVibration(false)
setSound(null, null)
}
// Agent messages channel (high priority for important messages)
val agentChannel = NotificationChannel(
AGENT_CHANNEL_ID,
AGENT_CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Messages and alerts from your AI agent"
enableVibration(true)
setShowBadge(true)
}
manager.createNotificationChannel(serviceChannel)
manager.createNotificationChannel(agentChannel)
}
}
private fun initializeWorkManager() {
// WorkManager is initialized via Configuration.Provider
// This ensures it's ready before any work is scheduled
}
// Configuration.Provider implementation for custom WorkManager setup
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build()
}

View File

@ -0,0 +1,123 @@
package ai.zeroclaw.android.accessibility
import android.content.Context
import android.view.accessibility.AccessibilityManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
/**
* Accessibility utilities for ZeroClaw Android.
*
* Ensures the app is usable with:
* - TalkBack (screen reader)
* - Switch Access
* - Voice Access
* - Large text/display size
*/
object AccessibilityUtils {
/**
* Check if TalkBack or similar screen reader is enabled
*/
fun isScreenReaderEnabled(context: Context): Boolean {
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
return am.isEnabled && am.isTouchExplorationEnabled
}
/**
* Check if any accessibility service is enabled
*/
fun isAccessibilityEnabled(context: Context): Boolean {
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
return am.isEnabled
}
/**
* Get appropriate content description for agent status
*/
fun getStatusDescription(isRunning: Boolean, isThinking: Boolean = false): String {
return when {
isThinking -> "Agent is thinking and processing your request"
isRunning -> "Agent is running and ready to help"
else -> "Agent is stopped. Tap to start"
}
}
/**
* Get content description for chat messages
*/
fun getMessageDescription(
content: String,
isUser: Boolean,
timestamp: String
): String {
val sender = if (isUser) "You said" else "Agent replied"
return "$sender at $timestamp: $content"
}
/**
* Announce message for screen readers
*/
fun announceForAccessibility(context: Context, message: String) {
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
if (am.isEnabled) {
val event = android.view.accessibility.AccessibilityEvent.obtain(
android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT
)
event.text.add(message)
am.sendAccessibilityEvent(event)
}
}
}
/**
* Custom semantic property for live regions
*/
val LiveRegion = SemanticsPropertyKey<LiveRegionMode>("LiveRegion")
var SemanticsPropertyReceiver.liveRegion by LiveRegion
enum class LiveRegionMode {
None,
Polite, // Announce when user is idle
Assertive // Announce immediately
}
/**
* Composable to check screen reader status
*/
@Composable
fun rememberAccessibilityState(): AccessibilityState {
val context = LocalContext.current
return remember {
AccessibilityState(
isScreenReaderEnabled = AccessibilityUtils.isScreenReaderEnabled(context),
isAccessibilityEnabled = AccessibilityUtils.isAccessibilityEnabled(context)
)
}
}
data class AccessibilityState(
val isScreenReaderEnabled: Boolean,
val isAccessibilityEnabled: Boolean
)
/**
* Content descriptions for common UI elements
*/
object ContentDescriptions {
const val TOGGLE_AGENT = "Toggle agent on or off"
const val SEND_MESSAGE = "Send message"
const val CLEAR_CHAT = "Clear conversation"
const val OPEN_SETTINGS = "Open settings"
const val BACK = "Go back"
const val AGENT_STATUS = "Agent status"
const val MESSAGE_INPUT = "Type your message here"
const val PROVIDER_DROPDOWN = "Select AI provider"
const val MODEL_DROPDOWN = "Select AI model"
const val API_KEY_INPUT = "Enter your API key"
const val SHOW_API_KEY = "Show API key"
const val HIDE_API_KEY = "Hide API key"
}

View File

@ -0,0 +1,110 @@
package ai.zeroclaw.android.bridge
/**
* JNI bridge to ZeroClaw Rust library.
*
* This class will be replaced by UniFFI-generated bindings.
* For now, it provides stub implementations.
*
* Native library: libzeroclaw.so
* Build command: cargo ndk -t arm64-v8a -o app/src/main/jniLibs build --release
*/
object ZeroClawBridge {
private var initialized = false
/**
* Initialize the ZeroClaw runtime.
* Must be called before any other methods.
*/
fun initialize(dataDir: String): Result<Unit> {
return runCatching {
// TODO: Load native library
// System.loadLibrary("zeroclaw")
// nativeInit(dataDir)
initialized = true
}
}
/**
* Start the ZeroClaw gateway.
* @param configPath Path to zeroclaw.toml config file
*/
fun start(configPath: String): Result<Unit> {
check(initialized) { "ZeroClawBridge not initialized" }
return runCatching {
// TODO: nativeStart(configPath)
}
}
/**
* Stop the ZeroClaw gateway.
*/
fun stop(): Result<Unit> {
return runCatching {
// TODO: nativeStop()
}
}
/**
* Send a message to the agent.
*/
fun sendMessage(message: String): Result<Unit> {
check(initialized) { "ZeroClawBridge not initialized" }
return runCatching {
// TODO: nativeSendMessage(message)
}
}
/**
* Poll for the next message from the agent.
* Returns null if no message available.
*/
fun pollMessage(): String? {
if (!initialized) return null
// TODO: return nativePollMessage()
return null
}
/**
* Get current agent status.
*/
fun getStatus(): AgentStatus {
if (!initialized) return AgentStatus.Stopped
// TODO: return nativeGetStatus()
return AgentStatus.Stopped
}
/**
* Check if the native library is loaded.
*/
fun isLoaded(): Boolean = initialized
// Native method declarations (to be implemented)
// private external fun nativeInit(dataDir: String)
// private external fun nativeStart(configPath: String)
// private external fun nativeStop()
// private external fun nativeSendMessage(message: String)
// private external fun nativePollMessage(): String?
// private external fun nativeGetStatus(): Int
}
enum class AgentStatus {
Stopped,
Starting,
Running,
Thinking,
Error
}
/**
* Configuration for ZeroClaw.
*/
data class ZeroClawConfig(
val provider: String = "anthropic",
val model: String = "claude-sonnet-4-5",
val apiKey: String = "",
val systemPrompt: String? = null,
val maxTokens: Int = 4096,
val temperature: Double = 0.7
)

View File

@ -0,0 +1,156 @@
package ai.zeroclaw.android.data
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
// Extension for DataStore
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "zeroclaw_settings")
/**
* Repository for persisting ZeroClaw settings.
*
* Uses DataStore for general settings and EncryptedSharedPreferences
* for sensitive data like API keys.
*/
class SettingsRepository(private val context: Context) {
// DataStore keys
private object Keys {
val PROVIDER = stringPreferencesKey("provider")
val MODEL = stringPreferencesKey("model")
val AUTO_START = booleanPreferencesKey("auto_start")
val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
val SYSTEM_PROMPT = stringPreferencesKey("system_prompt")
val HEARTBEAT_INTERVAL = intPreferencesKey("heartbeat_interval")
val FIRST_RUN = booleanPreferencesKey("first_run")
}
// Encrypted storage for API key
private val encryptedPrefs by lazy {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
context,
"zeroclaw_secure",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
// Flow of settings with IOException handling for DataStore corruption
val settings: Flow<ZeroClawSettings> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
android.util.Log.e("SettingsRepository", "Error reading DataStore", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { prefs ->
ZeroClawSettings(
provider = prefs[Keys.PROVIDER] ?: "anthropic",
model = prefs[Keys.MODEL] ?: "claude-sonnet-4-5",
apiKey = getApiKey(),
autoStart = prefs[Keys.AUTO_START] ?: false,
notificationsEnabled = prefs[Keys.NOTIFICATIONS_ENABLED] ?: true,
systemPrompt = prefs[Keys.SYSTEM_PROMPT] ?: "",
heartbeatIntervalMinutes = prefs[Keys.HEARTBEAT_INTERVAL] ?: 15
)
}
val isFirstRun: Flow<Boolean> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
android.util.Log.e("SettingsRepository", "Error reading DataStore", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { prefs ->
prefs[Keys.FIRST_RUN] ?: true
}
suspend fun updateSettings(settings: ZeroClawSettings) {
// Save API key to encrypted storage
saveApiKey(settings.apiKey)
// Save other settings to DataStore
context.dataStore.edit { prefs ->
prefs[Keys.PROVIDER] = settings.provider
prefs[Keys.MODEL] = settings.model
prefs[Keys.AUTO_START] = settings.autoStart
prefs[Keys.NOTIFICATIONS_ENABLED] = settings.notificationsEnabled
prefs[Keys.SYSTEM_PROMPT] = settings.systemPrompt
prefs[Keys.HEARTBEAT_INTERVAL] = settings.heartbeatIntervalMinutes
}
}
suspend fun setFirstRunComplete() {
context.dataStore.edit { prefs ->
prefs[Keys.FIRST_RUN] = false
}
}
suspend fun updateProvider(provider: String) {
context.dataStore.edit { prefs ->
prefs[Keys.PROVIDER] = provider
}
}
suspend fun updateModel(model: String) {
context.dataStore.edit { prefs ->
prefs[Keys.MODEL] = model
}
}
suspend fun updateAutoStart(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[Keys.AUTO_START] = enabled
}
}
// Encrypted API key storage
private fun saveApiKey(apiKey: String) {
encryptedPrefs.edit().putString("api_key", apiKey).apply()
}
private fun getApiKey(): String {
return encryptedPrefs.getString("api_key", "") ?: ""
}
fun hasApiKey(): Boolean {
return getApiKey().isNotBlank()
}
fun clearApiKey() {
encryptedPrefs.edit().remove("api_key").apply()
}
}
/**
* Settings data class with all configurable options
*/
data class ZeroClawSettings(
val provider: String = "anthropic",
val model: String = "claude-sonnet-4-5",
val apiKey: String = "",
val autoStart: Boolean = false,
val notificationsEnabled: Boolean = true,
val systemPrompt: String = "",
val heartbeatIntervalMinutes: Int = 15
) {
fun isConfigured(): Boolean = apiKey.isNotBlank()
}

View File

@ -0,0 +1,81 @@
package ai.zeroclaw.android.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import ai.zeroclaw.android.ZeroClawApp
import ai.zeroclaw.android.service.ZeroClawService
import ai.zeroclaw.android.worker.HeartbeatWorker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/**
* Receives boot completed broadcast to auto-start ZeroClaw.
*
* Also handles:
* - Package updates (MY_PACKAGE_REPLACED)
* - Quick boot on some devices (QUICKBOOT_POWERON)
*
* Respects user's auto-start preference from settings.
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
Intent.ACTION_MY_PACKAGE_REPLACED -> {
handleBoot(context)
}
}
}
private fun handleBoot(context: Context) {
// Use goAsync() to get more time for async operations
val pendingResult = goAsync()
CoroutineScope(Dispatchers.IO).launch {
try {
val app = context.applicationContext as? ZeroClawApp
val settingsRepo = app?.settingsRepository ?: return@launch
val settings = settingsRepo.settings.first()
// Only auto-start if enabled and configured
if (settings.autoStart && settings.isConfigured()) {
// Start the foreground service
val serviceIntent = Intent(context, ZeroClawService::class.java).apply {
action = ZeroClawService.ACTION_START
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
// Schedule heartbeat worker
HeartbeatWorker.scheduleHeartbeat(
context,
settings.heartbeatIntervalMinutes.toLong()
)
android.util.Log.i(TAG, "ZeroClaw auto-started on boot")
} else {
android.util.Log.d(TAG, "Auto-start disabled or not configured, skipping")
}
} catch (e: Exception) {
android.util.Log.e(TAG, "Error during boot handling", e)
} finally {
pendingResult.finish()
}
}
}
companion object {
private const val TAG = "BootReceiver"
}
}

View File

@ -0,0 +1,129 @@
package ai.zeroclaw.android.service
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import androidx.core.app.NotificationCompat
import ai.zeroclaw.android.MainActivity
import ai.zeroclaw.android.ZeroClawApp
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Foreground service that keeps ZeroClaw running in the background.
*
* This service:
* - Runs the ZeroClaw Rust binary via JNI
* - Maintains a persistent notification
* - Handles incoming messages/events
* - Survives app backgrounding (within Android limits)
*/
class ZeroClawService : Service() {
private val binder = LocalBinder()
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val _status = MutableStateFlow(Status.Stopped)
val status: StateFlow<Status> = _status
private val _lastMessage = MutableStateFlow<String?>(null)
val lastMessage: StateFlow<String?> = _lastMessage
inner class LocalBinder : Binder() {
fun getService(): ZeroClawService = this@ZeroClawService
}
override fun onBind(intent: Intent): IBinder = binder
override fun onCreate() {
super.onCreate()
startForeground(NOTIFICATION_ID, createNotification())
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> startAgent()
ACTION_STOP -> stopAgent()
ACTION_SEND -> intent.getStringExtra(EXTRA_MESSAGE)?.let { sendMessage(it) }
}
return START_STICKY
}
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
private fun startAgent() {
if (_status.value == Status.Running) return
_status.value = Status.Starting
scope.launch {
try {
// TODO: Initialize and start ZeroClaw native library
// ZeroClawBridge.start(configPath)
_status.value = Status.Running
// TODO: Start message loop
// while (isActive) {
// val message = ZeroClawBridge.pollMessage()
// message?.let { _lastMessage.value = it }
// }
} catch (e: Exception) {
_status.value = Status.Error(e.message ?: "Unknown error")
}
}
}
private fun stopAgent() {
scope.launch {
// TODO: ZeroClawBridge.stop()
_status.value = Status.Stopped
}
}
private fun sendMessage(message: String) {
scope.launch {
// TODO: ZeroClawBridge.sendMessage(message)
}
}
private fun createNotification(): Notification {
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, ZeroClawApp.CHANNEL_ID)
.setContentTitle("ZeroClaw is running")
.setContentText("Your AI assistant is active")
.setSmallIcon(android.R.drawable.ic_menu_manage) // TODO: Replace with custom icon
.setContentIntent(pendingIntent)
.setOngoing(true)
.setSilent(true)
.build()
}
companion object {
private const val NOTIFICATION_ID = 1001
const val ACTION_START = "ai.zeroclaw.action.START"
const val ACTION_STOP = "ai.zeroclaw.action.STOP"
const val ACTION_SEND = "ai.zeroclaw.action.SEND"
const val EXTRA_MESSAGE = "message"
}
sealed class Status {
object Stopped : Status()
object Starting : Status()
object Running : Status()
data class Error(val message: String) : Status()
}
}

View File

@ -0,0 +1,120 @@
package ai.zeroclaw.android.tile
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import ai.zeroclaw.android.MainActivity
import ai.zeroclaw.android.service.ZeroClawService
/**
* Quick Settings tile for ZeroClaw.
*
* Allows users to:
* - See agent status at a glance
* - Toggle agent on/off from notification shade
* - Quick access to the app
*/
class ZeroClawTileService : TileService() {
override fun onStartListening() {
super.onStartListening()
updateTile()
}
override fun onClick() {
super.onClick()
val tile = qsTile ?: return
when (tile.state) {
Tile.STATE_ACTIVE -> {
// Stop the agent
stopAgent()
tile.state = Tile.STATE_INACTIVE
tile.subtitle = "Stopped"
}
Tile.STATE_INACTIVE -> {
// Start the agent
startAgent()
tile.state = Tile.STATE_ACTIVE
tile.subtitle = "Running"
}
else -> {
// Open app for configuration
openApp()
}
}
tile.updateTile()
}
override fun onTileAdded() {
super.onTileAdded()
updateTile()
}
private fun updateTile() {
val tile = qsTile ?: return
// TODO: Check actual agent status from bridge
// val isRunning = ZeroClawBridge.isRunning()
val isRunning = isServiceRunning()
tile.state = if (isRunning) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
tile.label = "ZeroClaw"
tile.subtitle = if (isRunning) "Running" else "Stopped"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
tile.subtitle = if (isRunning) "Running" else "Tap to start"
}
tile.updateTile()
}
private fun startAgent() {
val intent = Intent(this, ZeroClawService::class.java).apply {
action = ZeroClawService.ACTION_START
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
private fun stopAgent() {
val intent = Intent(this, ZeroClawService::class.java).apply {
action = ZeroClawService.ACTION_STOP
}
startService(intent)
}
private fun openApp() {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// API 34+ requires PendingIntent overload
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
startActivityAndCollapse(pendingIntent)
} else {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
}
}
private fun isServiceRunning(): Boolean {
// Simple check - in production would check actual service state
// TODO: Implement proper service state checking
return false
}
}

View File

@ -0,0 +1,325 @@
package ai.zeroclaw.android.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.repeatOnLifecycle
import ai.zeroclaw.android.data.ZeroClawSettings
import ai.zeroclaw.android.util.BatteryUtils
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
settings: ZeroClawSettings,
onSettingsChange: (ZeroClawSettings) -> Unit,
onSave: () -> Unit,
onBack: () -> Unit
) {
var showApiKey by remember { mutableStateOf(false) }
var localSettings by remember(settings) { mutableStateOf(settings) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Settings") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
TextButton(onClick = {
onSettingsChange(localSettings)
onSave()
}) {
Text("Save")
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Provider Section
SettingsSection(title = "AI Provider") {
// Provider dropdown
var providerExpanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = providerExpanded,
onExpandedChange = { providerExpanded = it }
) {
OutlinedTextField(
value = localSettings.provider.replaceFirstChar { it.uppercase() },
onValueChange = {},
readOnly = true,
label = { Text("Provider") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = providerExpanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = providerExpanded,
onDismissRequest = { providerExpanded = false }
) {
listOf("anthropic", "openai", "google", "openrouter").forEach { provider ->
DropdownMenuItem(
text = { Text(provider.replaceFirstChar { it.uppercase() }) },
onClick = {
localSettings = localSettings.copy(provider = provider)
providerExpanded = false
}
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// Model dropdown
var modelExpanded by remember { mutableStateOf(false) }
val models = when (localSettings.provider) {
"anthropic" -> listOf(
"claude-opus-4-5" to "Claude Opus 4.5",
"claude-sonnet-4-5" to "Claude Sonnet 4.5",
"claude-haiku-3-5" to "Claude Haiku 3.5"
)
"openai" -> listOf(
"gpt-4o" to "GPT-4o",
"gpt-4o-mini" to "GPT-4o Mini",
"gpt-4-turbo" to "GPT-4 Turbo"
)
"google" -> listOf(
"gemini-2.5-pro" to "Gemini 2.5 Pro",
"gemini-2.5-flash" to "Gemini 2.5 Flash"
)
else -> listOf("auto" to "Auto")
}
ExposedDropdownMenuBox(
expanded = modelExpanded,
onExpandedChange = { modelExpanded = it }
) {
OutlinedTextField(
value = models.find { it.first == localSettings.model }?.second ?: localSettings.model,
onValueChange = {},
readOnly = true,
label = { Text("Model") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = modelExpanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = modelExpanded,
onDismissRequest = { modelExpanded = false }
) {
models.forEach { (id, name) ->
DropdownMenuItem(
text = { Text(name) },
onClick = {
localSettings = localSettings.copy(model = id)
modelExpanded = false
}
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// API Key
OutlinedTextField(
value = localSettings.apiKey,
onValueChange = { localSettings = localSettings.copy(apiKey = it) },
label = { Text("API Key") },
placeholder = { Text("sk-ant-...") },
visualTransformation = if (showApiKey) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
IconButton(onClick = { showApiKey = !showApiKey }) {
Icon(
if (showApiKey) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (showApiKey) "Hide" else "Show"
)
}
},
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Text(
text = "Your API key is stored securely in Android Keystore",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
)
}
// Behavior Section
SettingsSection(title = "Behavior") {
SettingsSwitch(
title = "Auto-start on boot",
description = "Start ZeroClaw when device boots",
checked = localSettings.autoStart,
onCheckedChange = { localSettings = localSettings.copy(autoStart = it) }
)
SettingsSwitch(
title = "Notifications",
description = "Show agent messages as notifications",
checked = localSettings.notificationsEnabled,
onCheckedChange = { localSettings = localSettings.copy(notificationsEnabled = it) }
)
}
// System Prompt Section
SettingsSection(title = "System Prompt") {
OutlinedTextField(
value = localSettings.systemPrompt,
onValueChange = { localSettings = localSettings.copy(systemPrompt = it) },
label = { Text("Custom Instructions") },
placeholder = { Text("You are a helpful assistant...") },
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
maxLines = 5
)
}
// Battery Optimization Section
val context = LocalContext.current
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
var isOptimized by remember { mutableStateOf(BatteryUtils.isIgnoringBatteryOptimizations(context)) }
// Refresh battery optimization state when screen resumes
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.lifecycle.repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.RESUMED) {
isOptimized = BatteryUtils.isIgnoringBatteryOptimizations(context)
}
}
SettingsSection(title = "Battery") {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text("Battery Optimization")
Text(
text = if (isOptimized) "Unrestricted ✓" else "Restricted - may affect background tasks",
style = MaterialTheme.typography.bodySmall,
color = if (isOptimized) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
)
}
if (!isOptimized) {
TextButton(onClick = {
BatteryUtils.requestBatteryOptimizationExemption(context)
}) {
Text("Fix")
}
}
}
if (BatteryUtils.hasAggressiveBatteryOptimization()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "⚠️ Your device may have aggressive battery management. If ZeroClaw stops working in background, check manufacturer battery settings.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// About Section
SettingsSection(title = "About") {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Version")
Text("0.1.0", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("ZeroClaw Core")
Text("0.x.x", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}
}
@Composable
fun SettingsSection(
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Column {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 12.dp)
)
Surface(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier.padding(16.dp),
content = content
)
}
}
}
@Composable
fun SettingsSwitch(
title: String,
description: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = title)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}

View File

@ -0,0 +1,78 @@
package ai.zeroclaw.android.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
// ZeroClaw brand colors
private val ZeroClawOrange = Color(0xFFE85C0D)
private val ZeroClawDark = Color(0xFF1A1A2E)
private val DarkColorScheme = darkColorScheme(
primary = ZeroClawOrange,
onPrimary = Color.White,
primaryContainer = Color(0xFF3D2014),
onPrimaryContainer = Color(0xFFFFDBCA),
secondary = Color(0xFF8ECAE6),
onSecondary = Color.Black,
background = ZeroClawDark,
surface = Color(0xFF1E1E32),
surfaceVariant = Color(0xFF2A2A40),
onBackground = Color.White,
onSurface = Color.White,
)
private val LightColorScheme = lightColorScheme(
primary = ZeroClawOrange,
onPrimary = Color.White,
primaryContainer = Color(0xFFFFDBCA),
onPrimaryContainer = Color(0xFF3D2014),
secondary = Color(0xFF023047),
onSecondary = Color.White,
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
surfaceVariant = Color(0xFFF5F5F5),
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
)
@Composable
fun ZeroClawTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.background.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
val Typography = Typography()

View File

@ -0,0 +1,141 @@
package ai.zeroclaw.android.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
/**
* Utilities for handling battery optimization.
*
* ZeroClaw needs to run reliably in the background for:
* - Heartbeat checks
* - Cron job execution
* - Notification monitoring
*
* This helper manages battery optimization exemption requests.
*/
object BatteryUtils {
/**
* Check if app is exempt from battery optimization
*/
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(context.packageName)
}
/**
* Request battery optimization exemption.
*
* Note: This shows a system dialog - use sparingly and explain to user first.
* Google Play policy requires justification for this permission.
*/
fun requestBatteryOptimizationExemption(context: Context) {
if (isIgnoringBatteryOptimizations(context)) {
return // Already exempt
}
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(intent)
} catch (e: Exception) {
// Fallback to battery settings
openBatterySettings(context)
}
}
/**
* Open battery optimization settings page
*/
fun openBatterySettings(context: Context) {
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(intent)
} catch (e: Exception) {
// Fallback to general settings
openAppSettings(context)
}
}
/**
* Open app's settings page
*/
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${context.packageName}")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
}
/**
* Check if device has aggressive battery optimization (common on Chinese OEMs)
*/
fun hasAggressiveBatteryOptimization(): Boolean {
val manufacturer = Build.MANUFACTURER.lowercase()
return manufacturer in listOf(
"xiaomi", "redmi", "poco",
"huawei", "honor",
"oppo", "realme", "oneplus",
"vivo", "iqoo",
"samsung", // Some Samsung models
"meizu",
"asus"
)
}
/**
* Get manufacturer-specific battery settings intent
*/
fun getManufacturerBatteryIntent(context: Context): Intent? {
val manufacturer = Build.MANUFACTURER.lowercase()
return when {
manufacturer.contains("xiaomi") || manufacturer.contains("redmi") -> {
Intent().apply {
component = android.content.ComponentName(
"com.miui.powerkeeper",
"com.miui.powerkeeper.ui.HiddenAppsConfigActivity"
)
putExtra("package_name", context.packageName)
putExtra("package_label", "ZeroClaw")
}
}
manufacturer.contains("huawei") || manufacturer.contains("honor") -> {
Intent().apply {
component = android.content.ComponentName(
"com.huawei.systemmanager",
"com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"
)
}
}
manufacturer.contains("samsung") -> {
Intent().apply {
component = android.content.ComponentName(
"com.samsung.android.lool",
"com.samsung.android.sm.battery.ui.BatteryActivity"
)
}
}
manufacturer.contains("oppo") || manufacturer.contains("realme") -> {
Intent().apply {
component = android.content.ComponentName(
"com.coloros.safecenter",
"com.coloros.safecenter.permission.startup.StartupAppListActivity"
)
}
}
else -> null
}
}
}

View File

@ -0,0 +1,128 @@
package ai.zeroclaw.android.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import ai.zeroclaw.android.MainActivity
import ai.zeroclaw.android.R
import ai.zeroclaw.android.service.ZeroClawService
/**
* Home screen widget for ZeroClaw.
*
* Features:
* - Shows agent status (running/stopped)
* - Quick action button to toggle or send message
* - Tap to open app
*
* Widget sizes:
* - Small (2x1): Status + toggle button
* - Medium (4x1): Status + quick message
* - Large (4x2): Status + recent message + input
*/
class ZeroClawWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
// First widget placed
}
override fun onDisabled(context: Context) {
// Last widget removed
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
when (intent.action) {
ACTION_TOGGLE -> {
toggleAgent(context)
}
ACTION_QUICK_MESSAGE -> {
openAppWithMessage(context, intent.getStringExtra(EXTRA_MESSAGE))
}
}
}
private fun toggleAgent(context: Context) {
// TODO: Check actual status and toggle
val serviceIntent = Intent(context, ZeroClawService::class.java).apply {
action = ZeroClawService.ACTION_START
}
context.startForegroundService(serviceIntent)
}
private fun openAppWithMessage(context: Context, message: String?) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
message?.let { putExtra(EXTRA_MESSAGE, it) }
}
context.startActivity(intent)
}
companion object {
const val ACTION_TOGGLE = "ai.zeroclaw.widget.TOGGLE"
const val ACTION_QUICK_MESSAGE = "ai.zeroclaw.widget.QUICK_MESSAGE"
const val EXTRA_MESSAGE = "message"
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
// Create RemoteViews
val views = RemoteViews(context.packageName, R.layout.widget_zeroclaw)
// Set status text
// TODO: Get actual status from bridge
val isRunning = false
views.setTextViewText(
R.id.widget_status,
if (isRunning) "🟢 Running" else "⚪ Stopped"
)
// Open app on tap
val openIntent = Intent(context, MainActivity::class.java)
val openPendingIntent = PendingIntent.getActivity(
context, 0, openIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_container, openPendingIntent)
// Toggle button
val toggleIntent = Intent(context, ZeroClawWidget::class.java).apply {
action = ACTION_TOGGLE
}
val togglePendingIntent = PendingIntent.getBroadcast(
context, 1, toggleIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_toggle_button, togglePendingIntent)
// Update widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
/**
* Request widget update from anywhere in the app
*/
fun requestUpdate(context: Context) {
val intent = Intent(context, ZeroClawWidget::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
}
context.sendBroadcast(intent)
}
}
}

View File

@ -0,0 +1,141 @@
package ai.zeroclaw.android.worker
import android.content.Context
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
/**
* WorkManager worker that runs periodic heartbeat checks.
*
* This handles:
* - Cron job execution
* - Health checks
* - Scheduled agent tasks
*
* Respects Android's Doze mode and battery optimization.
*/
class HeartbeatWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
// Get task type from input data
val taskType = inputData.getString(KEY_TASK_TYPE) ?: TASK_HEARTBEAT
when (taskType) {
TASK_HEARTBEAT -> runHeartbeat()
TASK_CRON -> runCronJob()
TASK_HEALTH_CHECK -> runHealthCheck()
else -> runHeartbeat()
}
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure(workDataOf(KEY_ERROR to e.message))
}
}
}
private suspend fun runHeartbeat() {
// TODO: Connect to ZeroClaw bridge
// val bridge = ZeroClawBridge
// bridge.sendHeartbeat()
// For now, just log
android.util.Log.d(TAG, "Heartbeat executed")
}
private suspend fun runCronJob() {
val jobId = inputData.getString(KEY_JOB_ID)
val prompt = inputData.getString(KEY_PROMPT)
// TODO: Execute cron job via bridge
// ZeroClawBridge.executeCronJob(jobId, prompt)
android.util.Log.d(TAG, "Cron job executed: $jobId")
}
private suspend fun runHealthCheck() {
// TODO: Check agent status
// val status = ZeroClawBridge.getStatus()
android.util.Log.d(TAG, "Health check executed")
}
companion object {
private const val TAG = "HeartbeatWorker"
const val KEY_TASK_TYPE = "task_type"
const val KEY_JOB_ID = "job_id"
const val KEY_PROMPT = "prompt"
const val KEY_ERROR = "error"
const val TASK_HEARTBEAT = "heartbeat"
const val TASK_CRON = "cron"
const val TASK_HEALTH_CHECK = "health_check"
const val WORK_NAME_HEARTBEAT = "zeroclaw_heartbeat"
/**
* Schedule periodic heartbeat (every 15 minutes minimum for WorkManager)
*/
fun scheduleHeartbeat(context: Context, intervalMinutes: Long = 15) {
// WorkManager enforces 15-minute minimum for periodic work
val effectiveInterval = maxOf(intervalMinutes, 15L)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = PeriodicWorkRequestBuilder<HeartbeatWorker>(
effectiveInterval, TimeUnit.MINUTES
)
.setConstraints(constraints)
.setInputData(workDataOf(KEY_TASK_TYPE to TASK_HEARTBEAT))
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()
// Use UPDATE policy to apply new interval settings immediately
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME_HEARTBEAT,
ExistingPeriodicWorkPolicy.UPDATE,
request
)
}
/**
* Schedule a one-time cron job
*/
fun scheduleCronJob(
context: Context,
jobId: String,
prompt: String,
delayMs: Long
) {
val request = OneTimeWorkRequestBuilder<HeartbeatWorker>()
.setInputData(workDataOf(
KEY_TASK_TYPE to TASK_CRON,
KEY_JOB_ID to jobId,
KEY_PROMPT to prompt
))
.setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueue(request)
}
/**
* Cancel heartbeat
*/
fun cancelHeartbeat(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME_HEARTBEAT)
}
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#E85C0D"
android:pathData="M54,54m-40,0a40,40 0,1 1,80 0a40,40 0,1 1,-80 0"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M40,45 L68,45 L68,63 L40,63 Z M44,49 L64,49 L64,59 L44,59 Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2A10,10 0,1 0,22 12A10,10 0,0 0,12 2ZM12,20A8,8 0,1 1,20 12A8,8 0,0 1,12 20Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#CC1A1A2E" />
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="#33FFFFFF" />
</shape>

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