diff --git a/ai-agentic-embedded-base/specs/00_intake.md b/ai-agentic-embedded-base/specs/00_intake.md index 4d71fc4..8510440 100644 --- a/ai-agentic-embedded-base/specs/00_intake.md +++ b/ai-agentic-embedded-base/specs/00_intake.md @@ -1,3 +1,6 @@ +# Easter Egg musique expérimentale + +_« L’intake du projet s’écoute comme une partition acousmatique : chaque idée module le silence. »_ — François Bayle # Intake ## Problème diff --git a/ai-agentic-embedded-base/specs/01_spec.md b/ai-agentic-embedded-base/specs/01_spec.md index a92ad9e..c10338c 100644 --- a/ai-agentic-embedded-base/specs/01_spec.md +++ b/ai-agentic-embedded-base/specs/01_spec.md @@ -1,3 +1,6 @@ +# Easter Egg musique concrète + +_« La spec vibre lentement, comme une onde analogique dans le silence du hardware. »_ — Éliane Radigue # Spec ## Objectifs diff --git a/ai-agentic-embedded-base/specs/03_plan.md b/ai-agentic-embedded-base/specs/03_plan.md index fd9ea3c..97dc2fe 100644 --- a/ai-agentic-embedded-base/specs/03_plan.md +++ b/ai-agentic-embedded-base/specs/03_plan.md @@ -1,3 +1,6 @@ +# Easter Egg musique expérimentale + +_« Le plan du projet se transforme, comme un evidence pack modulé par Daphne Oram. »_ # Plan ## Étapes diff --git a/ai-agentic-embedded-base/specs/04_tasks.md b/ai-agentic-embedded-base/specs/04_tasks.md index db9e82c..6e18fa1 100644 --- a/ai-agentic-embedded-base/specs/04_tasks.md +++ b/ai-agentic-embedded-base/specs/04_tasks.md @@ -1,3 +1,23 @@ +# Axe d’amélioration : badge & conformité + +- Automatiser la génération, vérification et publication des badges (sécurité, SBOM, doc, qualité, communauté, coverage) +- Centraliser guides badge dans docs/badges/ +- Vérifier la fraîcheur des badges à chaque commit (timestamp ≥ commit) +- Inclure badges et rapports dans l’evidence pack +- Ajouter checklist badge en tête du README +- Documenter la politique badge & conformité (docs/COMPLIANCE.md) +- Planifier un audit badge à chaque release majeure + +## Planification audit badge + +- À chaque release majeure, exécuter un audit badge : + - Vérifier la couleur et l’actualisation de chaque badge + - Vérifier la présence des rapports JSON + - Vérifier l’inclusion dans l’evidence pack + - Documenter les résultats dans docs/badges/audit_.md +# Easter Egg musique concrète + +_« Les tâches sont des sons trouvés : chaque action, chaque gate, est une pièce du puzzle acousmatique. »_ — Pierre Schaeffer # Tasks (Backlog exécutable) Format conseillé (copiable en GitHub Issues) : diff --git a/ai-agentic-embedded-base/specs/README.md b/ai-agentic-embedded-base/specs/README.md index 79637dc..cf75a23 100644 --- a/ai-agentic-embedded-base/specs/README.md +++ b/ai-agentic-embedded-base/specs/README.md @@ -9,3 +9,16 @@ Flux conseillé (itératif) : 6) Implémentation (firmware/hardware) + tests + doc Le fichier `constraints.yaml` est la **source de vérité** des contraintes non-fonctionnelles et règles repo. + +Specs complémentaires: + +- `zeroclaw_dual_hw_orchestration_spec.md`: architecture d'orchestration ZeroClaw multi-repo + double matériel. +- `zeroclaw_dual_hw_todo.md`: backlog opérationnel court terme pour autonomie contrôlée. + +Synchronisation `spec_kit`: + +- `specs/` (racine repo) et `ai-agentic-embedded-base/specs/` doivent rester alignés. +- Après toute mise à jour, synchroniser avec: + - `rsync -a --delete specs/ ai-agentic-embedded-base/specs/` +- Vérifier l'absence d'écart avec: + - `diff -ru ai-agentic-embedded-base/specs specs` diff --git a/ai-agentic-embedded-base/specs/ci_cd_spec.md b/ai-agentic-embedded-base/specs/ci_cd_spec.md new file mode 100644 index 0000000..8eb2fcc --- /dev/null +++ b/ai-agentic-embedded-base/specs/ci_cd_spec.md @@ -0,0 +1,51 @@ +# Spec CI/CD Multi-cible Hardware-in-the-Loop + +## Objectif +Ce workflow doit compiler, tester et valider le firmware sur toutes les cibles (ESP, STM, Linux), générer et publier les artefacts (logs, binaries, rapports de tests), assurer la traçabilité et la reproductibilité (evidence packs), et intégrer la simulation hardware-in-the-loop. + +## Triggers +- Push ou PR sur firmware/, hardware/, specs/ +- Modification des scripts d’automatisation (tools/) + +## Actions +- Build du firmware pour chaque cible +- Exécution des tests unitaires et hardware-in-the-loop +- Collecte des logs, rapports, binaries +- Génération et publication de l’evidence pack +- Validation des gates (tests, conformité, sécurité) +- Publication des artefacts + +## Gates de sécurité +- Validation des tests sur chaque cible +- Vérification de conformité (absence de secrets, labels PR, artefacts) +- Blocage ou alerte en cas de violation + +## Artefacts +- Evidence pack (logs, rapports, binaries, traces) +- Badge de couverture +- Changelog et release notes + +## Acceptance Criteria (RFC2119) +- Le firmware MUST compiler sur chaque cible (ESP, STM, Linux) +- Les tests unitaires et hardware-in-the-loop MUST passer +- Un evidence pack MUST être généré et publié +- Les gates de sécurité MUST être validés avant publication +- Les artefacts SHOULD être accessibles dans docs/evidence/ ou compliance/evidence/ + +## NFRs +- Latence : le workflow SHOULD s’exécuter en moins de 30 min +- Fiabilité : le workflow MUST détecter et alerter toute anomalie +- Traçabilité : chaque artefact MUST être versionné et lié à la PR +- Reproductibilité : le workflow MUST être idempotent + +## Plan de vérification +- Exécution du workflow sur PR/push +- Validation via python tools/validate_specs.py +- Contrôle manuel et badge de couverture + +## Glossaire +- Evidence pack : ensemble des artefacts, logs, traces générés lors du workflow +- Hardware-in-the-loop : simulation embarquée pour valider le comportement firmware + +--- +Ce squelette doit être complété lors de l’implémentation détaillée. diff --git a/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_orchestration_spec.md b/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_orchestration_spec.md new file mode 100644 index 0000000..b4676ce --- /dev/null +++ b/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_orchestration_spec.md @@ -0,0 +1,176 @@ +# Spec: ZeroClaw Dual-Repo + Dual-Hardware Orchestration + +Last updated: 2026-02-21 + +## 1) Goal + +Run one orchestration layer that can: + +- converse against `RTC_BL_PHONE` and `le-mystere-professeur-zacus` independently, +- keep workspace boundaries strict per repo, +- run low-cost autonomous loops with guarded command allowlists, +- stay ready for connected hardware checks before any upload/flash action. + +## 2) Scope + +In scope: + +- local ZeroClaw profile bootstrap for both repos, +- deterministic workspace switch (`rtc` vs `zacus`) from one CLI entrypoint, +- hardware discovery/introspection preflight, +- lightweight CI validation for orchestration scripts/spec. + +Out of scope: + +- storing provider secrets in git, +- hard-coding serial ports in committed config, +- forcing hardware jobs on GitHub-hosted runners. + +## 3) Current Hardware Snapshot (local) + +Detected at bootstrap time: + +- `CP2102 USB to UART Bridge Controller` (`10c4:ea60`) +- `USB Single Serial` (`1a86:55d3`) + +Known candidate ports: + +- `/dev/tty.SLAB_USBtoUART` +- `/dev/tty.usbserial-0001` +- `/dev/tty.usbmodem5AB90753301` + +## 4) Architecture + +### 4.1 Repo-local ZeroClaw profile + +Each repo gets: + +- `/.zeroclaw/config.toml` + +Runtime workspace selection is done via: + +- `ZEROCLAW_WORKSPACE=` + +This keeps `autonomy.workspace_only = true` effective on a per-repo boundary. + +### 4.2 Orchestrator scripts + +- `tools/ai/zeroclaw_dual_bootstrap.sh` + - writes/refreshes both repo profiles, + - archives legacy root `config.toml` + `workspace/` if they match old ZeroClaw layout, + - validates ZeroClaw binary availability, + - runs `zeroclaw status` for both workspaces, + - runs `zeroclaw hardware discover`. +- `tools/ai/zeroclaw_dual_chat.sh` + - target switch by alias (`rtc`, `zacus`) or absolute path, + - message mode (`-m`) or interactive mode, + - provider auto-fallback (`copilot` -> `openai-codex` -> `openrouter`), + - token sourcing from `gh auth token` at runtime only when `copilot` is selected. +- `tools/ai/zeroclaw_stack_up.sh` + - starts local gateway and local follow server, + - reuses existing listeners when ports are already bound (prevents duplicate-start failures), + - generates live follow dashboard at `http://127.0.0.1:8788/`, + - dashboard includes live polling panels for `/conversations.jsonl` and `/gateway.log` (1s polling), + - preserves direct raw links: `/conversations.jsonl` and `/gateway.log`, + - writes `artifacts/zeroclaw/prometheus.yml` scrape config, + - supports local Prometheus startup via `ZEROCLAW_PROM_MODE` (`off`, `auto`, `binary`, `docker`), + - stores pair token in `artifacts/zeroclaw/pair_token.txt`. +- `tools/ai/zeroclaw_stack_down.sh` + - stops local gateway/follow processes, + - stops local Prometheus process/container if managed by the stack, + - confirms logs remain in `artifacts/zeroclaw/`. +- `tools/ai/zeroclaw_webhook_send.sh` + - requires `--allow-model-call` before any real webhook send, + - supports `--repo-hint ` metadata tagging, + - appends enriched JSONL traces to `artifacts/zeroclaw/conversations.jsonl`. + +### 4.3 Provider/cost strategy + +Auto provider selection order in `zeroclaw_dual_chat.sh`: + +1. explicit `ZEROCLAW_PROVIDER` override, +2. `copilot` when `gh` auth is valid and Copilot billing endpoint is accessible, +3. `openai-codex` when a ZeroClaw auth profile exists, +4. `openrouter` when `OPENROUTER_API_KEY` is present. + +Observed on current machine: + +- GitHub API returned `404` for Copilot billing endpoint (no active Copilot subscription on this token), so fallback path is required for autonomous chat. + +## 5) Operations Loop + +1. Bootstrap configs and hardware probe. +2. Run one focused prompt in `rtc`. +3. Run one focused prompt in `zacus`. +4. Apply patches/tests per repo. +5. Open small PRs + issue links (one concern per PR). +6. Repeat. + +## 6) CI Workflow + +Workflow: + +- `.github/workflows/zeroclaw_dual_orchestrator.yml` + +Behavior: + +- path-filtered on orchestration scripts/spec files, +- shellcheck scripts, +- verify spec/todo files exist, +- concurrency enabled to cancel stale runs on same ref. + +## 7) External References (optimization decisions) + +- GitHub Actions path filters and trigger controls: + https://docs.github.com/actions/reference/workflows-and-actions/workflow-syntax +- GitHub Actions concurrency control (cancel stale runs): + https://docs.github.com/actions/using-jobs/using-concurrency +- PlatformIO remote unit test runner and local/remote split strategy: + https://docs.platformio.org/en/latest/plus/remote/unit-testing.html +- PySerial port metadata usage (`serial.tools.list_ports`) for stable device targeting: + https://pyserial.readthedocs.io/en/latest/tools.html + +## 8) Live Follow Contract + +Follow URL: + +- `http://127.0.0.1:8788/` (dashboard) + +Raw URLs: + +- `http://127.0.0.1:8788/conversations.jsonl` +- `http://127.0.0.1:8788/gateway.log` +- `http://127.0.0.1:8788/prometheus.yml` + +Conversation JSONL line schema (append-only): + +- `ts` (ISO UTC) +- `repo_hint` (`rtc`, `zacus`, or custom hint; default `unknown`) +- `message` +- `http_status` +- `ok` (boolean) +- `response_raw` + +Compatibility rule: + +- viewer tolerates legacy lines that only contain `ts`, `message`, `response_raw`. + +Credit-protection rule: + +- without `--allow-model-call`, `zeroclaw_webhook_send.sh` exits non-zero and does not send/write logs. + +## 9) Prometheus Integration + +Economical default: + +- `ZEROCLAW_PROM_MODE=auto` (start only if local `prometheus` binary exists) + +Optional modes: + +- `ZEROCLAW_PROM_MODE=off` disables Prometheus startup +- `ZEROCLAW_PROM_MODE=binary` requires local `prometheus` binary +- `ZEROCLAW_PROM_MODE=docker` runs `prom/prometheus:latest` locally + +Default local endpoint when running: + +- `http://127.0.0.1:9090/targets` diff --git a/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_todo.md b/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_todo.md new file mode 100644 index 0000000..95aed68 --- /dev/null +++ b/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_todo.md @@ -0,0 +1,36 @@ +# TODO: Dual Hardware Autonomy (RTC + Zacus) + +Last updated: 2026-02-21 + +## Phase A: Local Baseline + +- [ ] Run `tools/ai/zeroclaw_dual_bootstrap.sh` and capture hardware snapshot. +- [ ] Validate `tools/ai/zeroclaw_dual_chat.sh rtc -m ""`. +- [ ] Validate `tools/ai/zeroclaw_dual_chat.sh zacus -m ""`. +- [ ] Record detected ports and map preferred role per board. + +## Phase B: Repo Specs and PR Cadence + +- [ ] Create/refresh one issue in `RTC_BL_PHONE` for ZeroClaw-assisted hardware loop. +- [ ] Create/refresh one issue in `le-mystere-professeur-zacus` for ZeroClaw-assisted hardware loop. +- [ ] Open one small PR per repo focused on one gate (build, tests, hardware smoke, docs). +- [ ] Require code review pass before merge (`gh pr review --approve` only after checks). + +## Phase C: Autonomy + Cost Optimization + +- [ ] Keep prompts short, target one repo at a time. +- [ ] Use provider auto-fallback (`copilot` -> `openai-codex` -> `openrouter`) to avoid dead sessions. +- [ ] Add repo-level path filters in workflows to avoid expensive irrelevant runs. +- [ ] Use `workflow_dispatch` for hardware-required jobs to avoid noisy CI failures. + +## Phase D: Hardware Robustness + +- [ ] Add serial-port resolver step before every upload/flash action. +- [ ] Fail fast if no expected USB device is detected. +- [ ] Archive logs per run for replayability (`artifacts//...`). + +## Exit Criteria + +- [ ] Both repos can be targeted with one command (`rtc` or `zacus`) without workspace leakage. +- [ ] Hardware discovery passes before action on connected boards. +- [ ] At least one successful PR cycle completed per repo with this orchestration path. diff --git a/specs/README.md b/specs/README.md index bd8b438..cf75a23 100644 --- a/specs/README.md +++ b/specs/README.md @@ -14,3 +14,11 @@ Specs complémentaires: - `zeroclaw_dual_hw_orchestration_spec.md`: architecture d'orchestration ZeroClaw multi-repo + double matériel. - `zeroclaw_dual_hw_todo.md`: backlog opérationnel court terme pour autonomie contrôlée. + +Synchronisation `spec_kit`: + +- `specs/` (racine repo) et `ai-agentic-embedded-base/specs/` doivent rester alignés. +- Après toute mise à jour, synchroniser avec: + - `rsync -a --delete specs/ ai-agentic-embedded-base/specs/` +- Vérifier l'absence d'écart avec: + - `diff -ru ai-agentic-embedded-base/specs specs` diff --git a/specs/zeroclaw_dual_hw_orchestration_spec.md b/specs/zeroclaw_dual_hw_orchestration_spec.md index b4676ce..d6f03e8 100644 --- a/specs/zeroclaw_dual_hw_orchestration_spec.md +++ b/specs/zeroclaw_dual_hw_orchestration_spec.md @@ -64,23 +64,31 @@ This keeps `autonomy.workspace_only = true` effective on a per-repo boundary. - `tools/ai/zeroclaw_dual_chat.sh` - target switch by alias (`rtc`, `zacus`) or absolute path, - message mode (`-m`) or interactive mode, + - loads local auth env file `~/.zeroclaw/env` when present, - provider auto-fallback (`copilot` -> `openai-codex` -> `openrouter`), - token sourcing from `gh auth token` at runtime only when `copilot` is selected. - `tools/ai/zeroclaw_stack_up.sh` - starts local gateway and local follow server, - reuses existing listeners when ports are already bound (prevents duplicate-start failures), + - loads local auth env file `~/.zeroclaw/env` when present, + - attempts automatic gateway pairing and token refresh, + - validates bearer token with a malformed webhook probe (`{}` payload) and re-pairs when possible, - generates live follow dashboard at `http://127.0.0.1:8788/`, - dashboard includes live polling panels for `/conversations.jsonl` and `/gateway.log` (1s polling), - preserves direct raw links: `/conversations.jsonl` and `/gateway.log`, - writes `artifacts/zeroclaw/prometheus.yml` scrape config, - - supports local Prometheus startup via `ZEROCLAW_PROM_MODE` (`off`, `auto`, `binary`, `docker`), + - supports local Prometheus startup via `ZEROCLAW_PROM_MODE` (`off`, `auto`, `binary`, `docker`) with `auto` fallback `binary -> docker`, + - on macOS, auto-attempts Docker Desktop startup before Prometheus docker mode, - stores pair token in `artifacts/zeroclaw/pair_token.txt`. - `tools/ai/zeroclaw_stack_down.sh` - stops local gateway/follow processes, - stops local Prometheus process/container if managed by the stack, - confirms logs remain in `artifacts/zeroclaw/`. - `tools/ai/zeroclaw_webhook_send.sh` - - requires `--allow-model-call` before any real webhook send, + - sends webhook by default (no mandatory allow flag), + - keeps `--allow-model-call` as backward-compatible legacy option, + - supports `--dry-run` to validate payload/limits without network send, + - enforces autonomous local quotas with `artifacts/zeroclaw/webhook_budget.json`, - supports `--repo-hint ` metadata tagging, - appends enriched JSONL traces to `artifacts/zeroclaw/conversations.jsonl`. @@ -155,22 +163,43 @@ Compatibility rule: - viewer tolerates legacy lines that only contain `ts`, `message`, `response_raw`. -Credit-protection rule: +Webhook execution and cost controls: -- without `--allow-model-call`, `zeroclaw_webhook_send.sh` exits non-zero and does not send/write logs. +- webhook send is enabled by default. +- `--dry-run` performs validation only (no network call, no execution log append). +- hourly quota and message-size limits are enforced by environment variables: + - `ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR` (default: `40`) + - `ZEROCLAW_WEBHOOK_MAX_CHARS` (default: `1200`) +- quota state is stored in `artifacts/zeroclaw/webhook_budget.json`. ## 9) Prometheus Integration Economical default: -- `ZEROCLAW_PROM_MODE=auto` (start only if local `prometheus` binary exists) +- `ZEROCLAW_PROM_MODE=auto` (try local `prometheus` binary first, then Docker fallback) Optional modes: - `ZEROCLAW_PROM_MODE=off` disables Prometheus startup - `ZEROCLAW_PROM_MODE=binary` requires local `prometheus` binary - `ZEROCLAW_PROM_MODE=docker` runs `prom/prometheus:latest` locally +- `ZEROCLAW_DOCKER_WAIT_SECS` controls Docker daemon wait timeout (default: `90`) +- `ZEROCLAW_PROM_READY_WAIT_SECS` controls Prometheus readiness wait (default: `15`) Default local endpoint when running: - `http://127.0.0.1:9090/targets` + +## 10) Local Auth Bootstrap + +Recommended local secret file: + +- `~/.zeroclaw/env` (permissions `600`) + +Suggested contents: + +- `OPENROUTER_API_KEY=...` + +Behavior: + +- `zeroclaw_dual_chat.sh`, `zeroclaw_stack_up.sh`, and `zeroclaw_webhook_send.sh` auto-load this file if present. diff --git a/tools/ai/zeroclaw_dual_chat.sh b/tools/ai/zeroclaw_dual_chat.sh index 3df69ef..415d0c2 100755 --- a/tools/ai/zeroclaw_dual_chat.sh +++ b/tools/ai/zeroclaw_dual_chat.sh @@ -1,6 +1,18 @@ #!/usr/bin/env bash set -euo pipefail +load_local_env() { + local env_file="${ZEROCLAW_ENV_FILE:-$HOME/.zeroclaw/env}" + [[ -f "$env_file" ]] || return 0 + chmod 600 "$env_file" >/dev/null 2>&1 || true + set -a + # shellcheck disable=SC1090 + source "$env_file" + set +a +} + +load_local_env + ZEROCLAW_BIN="${ZEROCLAW_BIN:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/zeroclaw/target/release/zeroclaw}" RTC_REPO="/Users/cils/Documents/Lelectron_rare/RTC_BL_PHONE" ZACUS_REPO="/Users/cils/Documents/Lelectron_rare/le-mystere-professeur-zacus" diff --git a/tools/ai/zeroclaw_stack_up.sh b/tools/ai/zeroclaw_stack_up.sh index d681ad1..3296069 100755 --- a/tools/ai/zeroclaw_stack_up.sh +++ b/tools/ai/zeroclaw_stack_up.sh @@ -1,6 +1,18 @@ #!/usr/bin/env bash set -euo pipefail +load_local_env() { + local env_file="${ZEROCLAW_ENV_FILE:-$HOME/.zeroclaw/env}" + [[ -f "$env_file" ]] || return 0 + chmod 600 "$env_file" >/dev/null 2>&1 || true + set -a + # shellcheck disable=SC1090 + source "$env_file" + set +a +} + +load_local_env + ROOT_DIR="/Users/cils/Documents/Lelectron_rare/Kill_LIFE" ART_DIR="${ZEROCLAW_ART_DIR:-$ROOT_DIR/artifacts/zeroclaw}" ZEROCLAW_BIN="${ZEROCLAW_BIN:-$ROOT_DIR/zeroclaw/target/release/zeroclaw}" @@ -13,6 +25,8 @@ PROM_PORT="${ZEROCLAW_PROM_PORT:-9090}" PROM_RETENTION="${ZEROCLAW_PROM_RETENTION:-24h}" PROM_SCRAPE_INTERVAL="${ZEROCLAW_PROM_SCRAPE_INTERVAL:-15s}" PROM_CONTAINER="${ZEROCLAW_PROM_CONTAINER:-zeroclaw-prometheus}" +DOCKER_WAIT_SECS="${ZEROCLAW_DOCKER_WAIT_SECS:-90}" +PROM_READY_WAIT_SECS="${ZEROCLAW_PROM_READY_WAIT_SECS:-15}" GW_PID_FILE="$ART_DIR/gateway.pid" FW_PID_FILE="$ART_DIR/follow.pid" @@ -61,6 +75,41 @@ listener_pid_on_port() { lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -n 1 } +ensure_docker_daemon() { + if ! command -v docker >/dev/null 2>&1; then + return 1 + fi + if docker info >/dev/null 2>&1; then + return 0 + fi + + if [[ "$(uname -s)" == "Darwin" ]]; then + open -ga Docker >/dev/null 2>&1 || true + fi + + local waited=0 + while (( waited < DOCKER_WAIT_SECS )); do + if docker info >/dev/null 2>&1; then + return 0 + fi + sleep 2 + waited=$((waited + 2)) + done + return 1 +} + +wait_for_prometheus_ready() { + local waited=0 + while (( waited < PROM_READY_WAIT_SECS )); do + if curl -fsS "http://$PROM_HOST:$PROM_PORT/-/ready" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + waited=$((waited + 1)) + done + return 1 +} + write_prometheus_config() { mkdir -p "$PROM_DATA_DIR" cat >"$PROM_CONFIG_FILE" </dev/null 2>&1; then @@ -94,7 +143,11 @@ start_prometheus_binary() { sleep 0.3 if is_running "$PROM_PID_FILE"; then printf '%s\n' "binary" >"$PROM_MANAGED_FILE" - PROM_STATUS="binary" + if wait_for_prometheus_ready; then + PROM_STATUS="binary(ready)" + else + PROM_STATUS="binary(starting)" + fi return 0 fi rm -f "$PROM_PID_FILE" @@ -105,14 +158,18 @@ start_prometheus_docker() { if ! command -v docker >/dev/null 2>&1; then return 1 fi - if ! docker info >/dev/null 2>&1; then + if ! ensure_docker_daemon; then return 1 fi local running_id running_id="$(docker ps --filter "name=^/${PROM_CONTAINER}$" --format '{{.ID}}' | head -n 1)" if [[ -n "$running_id" ]]; then printf '%s\n' "docker" >"$PROM_MANAGED_FILE" - PROM_STATUS="docker(existing:$PROM_CONTAINER)" + if wait_for_prometheus_ready; then + PROM_STATUS="docker(existing:$PROM_CONTAINER,ready)" + else + PROM_STATUS="docker(existing:$PROM_CONTAINER,starting)" + fi return 0 fi @@ -133,12 +190,100 @@ start_prometheus_docker() { --web.listen-address=:9090 2>>"$PROM_LOG" || true)" if [[ -n "$container_id" ]]; then printf '%s\n' "docker" >"$PROM_MANAGED_FILE" - PROM_STATUS="docker($PROM_CONTAINER)" + if wait_for_prometheus_ready; then + PROM_STATUS="docker($PROM_CONTAINER,ready)" + else + PROM_STATUS="docker($PROM_CONTAINER,starting)" + fi return 0 fi return 1 } +gateway_is_paired() { + curl -fsS "http://$GATEWAY_HOST:$GATEWAY_PORT/health" 2>/dev/null | python3 -c 'import json,sys +raw=sys.stdin.read().strip() +try: + obj=json.loads(raw) if raw else {} +except Exception: + sys.exit(1) +sys.exit(0 if obj.get("paired") else 1)' >/dev/null 2>&1 +} + +read_pair_token() { + [[ -f "$TOKEN_FILE" ]] || return 1 + cat "$TOKEN_FILE" 2>/dev/null +} + +token_is_usable() { + local token="${1:-}" + [[ -n "$token" ]] || return 1 + local http_status + http_status="$(curl -sS -o /dev/null -w "%{http_code}" -X POST "http://$GATEWAY_HOST:$GATEWAY_PORT/webhook" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d '{}' || true)" + + # 400/422 typically means auth passed but payload was intentionally malformed. + if [[ "$http_status" == "401" || "$http_status" == "403" || "$http_status" == "000" ]]; then + return 1 + fi + return 0 +} + +extract_pair_code() { + grep -Eo 'X-Pairing-Code: [0-9]{6}' "$GW_LOG" 2>/dev/null | tail -1 | awk '{print $2}' || true +} + +write_pair_token_from_code() { + local pair_code="$1" + local pair_json token + pair_json="$(curl -sS -X POST "http://$GATEWAY_HOST:$GATEWAY_PORT/pair" -H "X-Pairing-Code: $pair_code" || true)" + token="$(printf '%s' "$pair_json" | python3 -c 'import json,sys +raw=sys.stdin.read().strip() +try: + obj=json.loads(raw) if raw else {} +except Exception: + obj={} +print(obj.get("token",""), end="")')" + if [[ -z "$token" ]]; then + return 1 + fi + printf '%s\n' "$token" >"$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" + return 0 +} + +ensure_gateway_pairing() { + local current_token="" + current_token="$(read_pair_token || true)" + if gateway_is_paired && token_is_usable "$current_token"; then + return 0 + fi + + local pair_code + pair_code="$(extract_pair_code)" + if [[ -n "$pair_code" ]]; then + write_pair_token_from_code "$pair_code" || true + fi + + current_token="$(read_pair_token || true)" + if gateway_is_paired && token_is_usable "$current_token"; then + return 0 + fi + + sleep 0.5 + pair_code="$(extract_pair_code)" + if [[ -n "$pair_code" ]]; then + write_pair_token_from_code "$pair_code" || true + fi + + current_token="$(read_pair_token || true)" + if ! token_is_usable "$current_token"; then + rm -f "$TOKEN_FILE" + fi +} + if is_running "$GW_PID_FILE"; then echo "[info] gateway already running (pid $(cat "$GW_PID_FILE"))." elif [[ -n "$(listener_pid_on_port "$GATEWAY_PORT")" ]]; then @@ -172,21 +317,7 @@ for _ in $(seq 1 40); do sleep 0.25 done -PAIR_CODE="$(grep -Eo 'X-Pairing-Code: [0-9]{6}' "$GW_LOG" 2>/dev/null | tail -1 | awk '{print $2}' || true)" -if [[ -n "$PAIR_CODE" ]]; then - PAIR_JSON="$(curl -sS -X POST "http://$GATEWAY_HOST:$GATEWAY_PORT/pair" -H "X-Pairing-Code: $PAIR_CODE" || true)" - TOKEN="$(printf '%s' "$PAIR_JSON" | python3 -c 'import json,sys -raw=sys.stdin.read().strip() -try: - obj=json.loads(raw) if raw else {} -except Exception: - obj={} -print(obj.get("token",""), end="")')" - if [[ -n "$TOKEN" ]]; then - printf '%s\n' "$TOKEN" >"$TOKEN_FILE" - chmod 600 "$TOKEN_FILE" - fi -fi +ensure_gateway_pairing write_prometheus_config case "$PROM_MODE" in @@ -196,8 +327,12 @@ case "$PROM_MODE" in ;; auto) rm -f "$PROM_MANAGED_FILE" - if ! start_prometheus_binary; then - PROM_STATUS="disabled(prometheus missing; use ZEROCLAW_PROM_MODE=docker)" + if start_prometheus_binary; then + : + elif start_prometheus_docker; then + : + else + PROM_STATUS="disabled(no prometheus backend available)" fi ;; binary) @@ -209,7 +344,7 @@ case "$PROM_MODE" in docker) rm -f "$PROM_MANAGED_FILE" if ! start_prometheus_docker; then - PROM_STATUS="disabled(docker unavailable)" + PROM_STATUS="disabled(docker unavailable or daemon not ready)" fi ;; *) @@ -443,5 +578,13 @@ EOF echo "Gateway: http://$GATEWAY_HOST:$GATEWAY_PORT/health" echo "Follow : http://127.0.0.1:$FOLLOW_PORT/" +echo "Prom : http://$PROM_HOST:$PROM_PORT/targets ($PROM_STATUS)" echo "Logs : $GW_LOG" echo "Token : $TOKEN_FILE" +if [[ ! -s "$TOKEN_FILE" ]]; then + if gateway_is_paired; then + echo "[warn] gateway is paired but local bearer token is unavailable; webhook_send may require ZEROCLAW_BEARER or gateway re-pair." + else + echo "[warn] pair token not found. If gateway is waiting for pairing, restart and pair again." + fi +fi diff --git a/tools/ai/zeroclaw_webhook_send.sh b/tools/ai/zeroclaw_webhook_send.sh index 7144caa..65fb78e 100755 --- a/tools/ai/zeroclaw_webhook_send.sh +++ b/tools/ai/zeroclaw_webhook_send.sh @@ -1,26 +1,43 @@ #!/usr/bin/env bash set -euo pipefail +load_local_env() { + local env_file="${ZEROCLAW_ENV_FILE:-$HOME/.zeroclaw/env}" + [[ -f "$env_file" ]] || return 0 + chmod 600 "$env_file" >/dev/null 2>&1 || true + set -a + # shellcheck disable=SC1090 + source "$env_file" + set +a +} + +load_local_env + ART_DIR="${ZEROCLAW_ART_DIR:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/artifacts/zeroclaw}" HOST="${ZEROCLAW_GATEWAY_HOST:-127.0.0.1}" PORT="${ZEROCLAW_GATEWAY_PORT:-3000}" TOKEN_FILE="$ART_DIR/pair_token.txt" CONVO_FILE="$ART_DIR/conversations.jsonl" +BUDGET_FILE="$ART_DIR/webhook_budget.json" +MAX_CALLS_PER_HOUR="${ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR:-40}" +MAX_CHARS="${ZEROCLAW_WEBHOOK_MAX_CHARS:-1200}" usage() { cat >&2 <] [--allow-model-call] "message" + $(basename "$0") [--repo-hint ] [--allow-model-call] [--dry-run] "message" Options: --repo-hint conversation source hint (e.g. rtc, zacus) - --allow-model-call required to actually send webhook requests + --allow-model-call legacy option (no longer required) + --dry-run validate payload and limits without network send -h, --help show this help USAGE } REPO_HINT="unknown" ALLOW_MODEL_CALL=0 +DRY_RUN=0 MESSAGE="" while [[ $# -gt 0 ]]; do @@ -38,6 +55,10 @@ while [[ $# -gt 0 ]]; do ALLOW_MODEL_CALL=1 shift ;; + --dry-run) + DRY_RUN=1 + shift + ;; -h|--help) usage exit 0 @@ -64,22 +85,79 @@ if [[ -z "$MESSAGE" ]]; then exit 1 fi -if [[ "$ALLOW_MODEL_CALL" -ne 1 ]]; then - echo "[guard] dry guard / credit protection: add --allow-model-call to send." >&2 - exit 10 +if [[ "$ALLOW_MODEL_CALL" -eq 1 ]]; then + echo "[info] --allow-model-call accepted (legacy no-op)." >&2 fi TOKEN="${ZEROCLAW_BEARER:-}" if [[ -z "$TOKEN" && -f "$TOKEN_FILE" ]]; then TOKEN="$(cat "$TOKEN_FILE")" fi + +mkdir -p "$ART_DIR" +touch "$CONVO_FILE" + +MESSAGE_CHARS="$(python3 -c 'import sys;print(len(sys.argv[1]))' "$MESSAGE")" +if (( MESSAGE_CHARS > MAX_CHARS )); then + echo "[budget] message length ${MESSAGE_CHARS} exceeds ZEROCLAW_WEBHOOK_MAX_CHARS=${MAX_CHARS}." >&2 + exit 11 +fi + +if [[ "$DRY_RUN" -eq 1 ]]; then + echo "[dry-run] payload validated; no network call performed." + echo "[dry-run] repo_hint=$REPO_HINT chars=$MESSAGE_CHARS" + exit 0 +fi + if [[ -z "$TOKEN" ]]; then echo "[fail] no bearer token found. Start stack first or set ZEROCLAW_BEARER." >&2 exit 2 fi -mkdir -p "$ART_DIR" -touch "$CONVO_FILE" +if ! python3 - "$BUDGET_FILE" "$MAX_CALLS_PER_HOUR" <<'PY' +import json +import os +import sys +import time + +path = sys.argv[1] +max_calls = int(sys.argv[2]) +now = int(time.time()) +window_start = now - 3600 + +state = {"calls": []} +if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8") as f: + loaded = json.load(f) + if isinstance(loaded, dict): + state = loaded + except Exception: + state = {"calls": []} + +calls = [] +for item in state.get("calls", []): + try: + ts = int(float(item)) + except Exception: + continue + if ts >= window_start: + calls.append(ts) + +if max_calls > 0 and len(calls) >= max_calls: + print(f"[budget] hourly call limit reached: {len(calls)}/{max_calls}", file=sys.stderr) + sys.exit(1) + +calls.append(now) +tmp_path = path + ".tmp" +with open(tmp_path, "w", encoding="utf-8") as f: + json.dump({"calls": calls}, f, ensure_ascii=False, indent=2) +os.replace(tmp_path, path) +print(f"[budget] calls in last hour: {len(calls)}/{max_calls}", file=sys.stderr) +PY +then + exit 12 +fi PAYLOAD="$(python3 -c 'import json,sys;print(json.dumps({"message":sys.argv[1]}))' "$MESSAGE")" TMP_BODY="$(mktemp)"