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:
commit
0aade407fa
19
.cargo/armv6l-unknown-linux-musleabihf.json
Normal file
19
.cargo/armv6l-unknown-linux-musleabihf.json
Normal 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"
|
||||
}
|
||||
@ -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"]
|
||||
|
||||
@ -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
50
.github/CODEOWNERS
vendored
@ -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
|
||||
|
||||
7
.github/actionlint.yaml
vendored
7
.github/actionlint.yaml
vendored
@ -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
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -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"
|
||||
|
||||
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@ -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)
|
||||
|
||||
|
||||
1
.github/release/prerelease-stage-gates.json
vendored
1
.github/release/prerelease-stage-gates.json
vendored
@ -23,7 +23,6 @@
|
||||
"Nightly Summary & Routing"
|
||||
],
|
||||
"stable": [
|
||||
"Main Promotion Gate",
|
||||
"CI Required Gate",
|
||||
"Security Audit",
|
||||
"Feature Matrix Summary",
|
||||
|
||||
@ -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"
|
||||
|
||||
13
.github/security/deny-ignore-governance.json
vendored
13
.github/security/deny-ignore-governance.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
1
.github/workflows/README.md
vendored
1
.github/workflows/README.md
vendored
@ -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`
|
||||
|
||||
83
.github/workflows/auto-main-release-tag.yml
vendored
Normal file
83
.github/workflows/auto-main-release-tag.yml
vendored
Normal 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"
|
||||
61
.github/workflows/ci-build-fast.yml
vendored
61
.github/workflows/ci-build-fast.yml
vendored
@ -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
|
||||
13
.github/workflows/ci-canary-gate.yml
vendored
13
.github/workflows/ci-canary-gate.yml
vendored
@ -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
|
||||
|
||||
36
.github/workflows/ci-change-audit.yml
vendored
36
.github/workflows/ci-change-audit.yml
vendored
@ -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
|
||||
|
||||
68
.github/workflows/ci-connectivity-probes.yml
vendored
68
.github/workflows/ci-connectivity-probes.yml
vendored
@ -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
|
||||
88
.github/workflows/ci-post-release-validation.yml
vendored
Normal file
88
.github/workflows/ci-post-release-validation.yml
vendored
Normal 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"
|
||||
@ -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
152
.github/workflows/ci-queue-hygiene.yml
vendored
Normal 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
|
||||
51
.github/workflows/ci-reproducible-build.yml
vendored
51
.github/workflows/ci-reproducible-build.yml
vendored
@ -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: |
|
||||
|
||||
19
.github/workflows/ci-rollback.yml
vendored
19
.github/workflows/ci-rollback.yml
vendored
@ -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
|
||||
|
||||
364
.github/workflows/ci-run.yml
vendored
364
.github/workflows/ci-run.yml
vendored
@ -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."
|
||||
|
||||
57
.github/workflows/ci-supply-chain-provenance.yml
vendored
57
.github/workflows/ci-supply-chain-provenance.yml
vendored
@ -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
56
.github/workflows/deploy-web.yml
vendored
Normal 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
|
||||
184
.github/workflows/docs-deploy.yml
vendored
184
.github/workflows/docs-deploy.yml
vendored
@ -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
|
||||
|
||||
83
.github/workflows/feature-matrix.yml
vendored
83
.github/workflows/feature-matrix.yml
vendored
@ -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
|
||||
|
||||
|
||||
61
.github/workflows/main-branch-flow.md
vendored
61
.github/workflows/main-branch-flow.md
vendored
@ -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.
|
||||
|
||||
34
.github/workflows/main-promotion-gate.yml
vendored
34
.github/workflows/main-promotion-gate.yml
vendored
@ -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"
|
||||
41
.github/workflows/nightly-all-features.yml
vendored
41
.github/workflows/nightly-all-features.yml
vendored
@ -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
|
||||
|
||||
|
||||
4
.github/workflows/pages-deploy.yml
vendored
4
.github/workflows/pages-deploy.yml
vendored
@ -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 }}
|
||||
|
||||
20
.github/workflows/pr-auto-response.yml
vendored
20
.github/workflows/pr-auto-response.yml
vendored
@ -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
|
||||
|
||||
8
.github/workflows/pr-check-stale.yml
vendored
8
.github/workflows/pr-check-stale.yml
vendored
@ -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
|
||||
|
||||
9
.github/workflows/pr-check-status.yml
vendored
9
.github/workflows/pr-check-status.yml
vendored
@ -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:
|
||||
|
||||
10
.github/workflows/pr-intake-checks.yml
vendored
10
.github/workflows/pr-intake-checks.yml
vendored
@ -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
|
||||
|
||||
9
.github/workflows/pr-label-policy-check.yml
vendored
9
.github/workflows/pr-label-policy-check.yml
vendored
@ -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
|
||||
|
||||
7
.github/workflows/pr-labeler.yml
vendored
7
.github/workflows/pr-labeler.yml
vendored
@ -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
|
||||
|
||||
124
.github/workflows/pub-docker-img.yml
vendored
124
.github/workflows/pub-docker-img.yml
vendored
@ -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
|
||||
|
||||
11
.github/workflows/pub-prerelease.yml
vendored
11
.github/workflows/pub-prerelease.yml
vendored
@ -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
|
||||
|
||||
415
.github/workflows/pub-release.yml
vendored
415
.github/workflows/pub-release.yml
vendored
@ -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
103
.github/workflows/release-build.yml
vendored
Normal 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
|
||||
61
.github/workflows/scripts/ci_human_review_guard.js
vendored
Normal file
61
.github/workflows/scripts/ci_human_review_guard.js
vendored
Normal 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(", ")}`);
|
||||
};
|
||||
@ -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}`);
|
||||
|
||||
};
|
||||
8
.github/workflows/scripts/lint_feedback.js
vendored
8
.github/workflows/scripts/lint_feedback.js
vendored
@ -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.");
|
||||
|
||||
10
.github/workflows/scripts/pr_intake_checks.js
vendored
10
.github/workflows/scripts/pr_intake_checks.js
vendored
@ -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) {
|
||||
|
||||
280
.github/workflows/sec-audit.yml
vendored
280
.github/workflows/sec-audit.yml
vendored
@ -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."
|
||||
|
||||
69
.github/workflows/sec-codeql.yml
vendored
69
.github/workflows/sec-codeql.yml
vendored
@ -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"
|
||||
|
||||
8
.github/workflows/sec-vorpal-reviewdog.yml
vendored
8
.github/workflows/sec-vorpal-reviewdog.yml
vendored
@ -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
|
||||
|
||||
3
.github/workflows/sync-contributors.yml
vendored
3
.github/workflows/sync-contributors.yml
vendored
@ -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
|
||||
|
||||
5
.github/workflows/test-benchmarks.yml
vendored
5
.github/workflows/test-benchmarks.yml
vendored
@ -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
|
||||
|
||||
46
.github/workflows/test-e2e.yml
vendored
46
.github/workflows/test-e2e.yml
vendored
@ -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
|
||||
|
||||
5
.github/workflows/test-fuzz.yml
vendored
5
.github/workflows/test-fuzz.yml
vendored
@ -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
|
||||
|
||||
62
.github/workflows/test-rust-build.yml
vendored
62
.github/workflows/test-rust-build.yml
vendored
@ -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
90
.github/workflows/test-self-hosted.yml
vendored
Normal 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
|
||||
49
.github/workflows/workflow-sanity.yml
vendored
49
.github/workflows/workflow-sanity.yml
vendored
@ -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
11
.gitignore
vendored
@ -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
|
||||
|
||||
23
AGENTS.md
23
AGENTS.md
@ -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:
|
||||
|
||||
@ -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
111
CLAUDE.md
@ -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
705
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
26
Dockerfile
26
Dockerfile
@ -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
51
PR_DESCRIPTION_UPDATE.md
Normal 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
|
||||
884
README.fr.md
884
README.fr.md
@ -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>
|
||||
300
README.ja.md
300
README.ja.md
@ -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)、[Reddit(r/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 トークンを他の製品・ツール・サービス(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** |
|
||||
| **バイナリサイズ** | ~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 ランタイムが必要で、ランタイム由来だけで通常は約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) を参照してください。
|
||||
|
||||
## クイックスタート
|
||||
|
||||
### 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
|
||||
# サーバー/ヘッドレス環境向け推奨
|
||||
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) を参照してください。
|
||||
300
README.ru.md
300
README.ru.md
@ -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).
|
||||
1060
README.vi.md
1060
README.vi.md
File diff suppressed because it is too large
Load Diff
305
README.zh-CN.md
305
README.zh-CN.md
@ -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)、[Reddit(r/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 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** |
|
||||
| **二进制体积** | ~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 运行时环境,仅该运行时通常就会带来约 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 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)。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 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!"
|
||||
|
||||
# 启动网关(默认: 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
|
||||
# 推荐用于服务器/无显示器环境
|
||||
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 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 添加;不支持的类型会快速失败 |
|
||||
| **安全** | `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)。
|
||||
@ -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`
|
||||
|
||||
14
SECURITY.md
14
SECURITY.md
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
19
build_and_run.sh
Normal 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
8
build_release.sh
Normal 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"
|
||||
43
clients/android-bridge/Cargo.toml
Normal file
43
clients/android-bridge/Cargo.toml
Normal 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
|
||||
305
clients/android-bridge/src/lib.rs
Normal file
305
clients/android-bridge/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
||||
5
clients/android-bridge/uniffi-bindgen.rs
Normal file
5
clients/android-bridge/uniffi-bindgen.rs
Normal file
@ -0,0 +1,5 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
108
clients/android/README.md
Normal file
108
clients/android/README.md
Normal 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
97
clients/android/SIZE.md
Normal 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.
|
||||
140
clients/android/app/build.gradle.kts
Normal file
140
clients/android/app/build.gradle.kts
Normal 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
67
clients/android/app/proguard-rules.pro
vendored
Normal 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
|
||||
129
clients/android/app/src/main/AndroidManifest.xml
Normal file
129
clients/android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@ -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
|
||||
}
|
||||
@ -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 -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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()
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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
Loading…
Reference in New Issue
Block a user