From 46e1feba84e3825178508b858026aad0976f59cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20SAILLANT?= <108685187+electron-rare@users.noreply.github.com> Date: Sun, 8 Mar 2026 03:28:31 +0100 Subject: [PATCH] feat: integrate local CAD stack and runtime safeguards --- .github/workflows/ci.yml | 29 ++++ .github/workflows/release_signing.yml | 26 +++- .gitignore | 1 + README.md | 49 +++++- ai-agentic-embedded-base/specs/04_tasks.md | 10 +- ai-agentic-embedded-base/specs/mcp_tasks.md | 15 +- .../specs/zeroclaw_dual_hw_todo.md | 7 +- docs/GITHUB_PAGES_SETUP.md | 42 +++-- docs/LOCAL_CHANGE_BUNDLES_2026-03-08.md | 92 +++++++++++ docs/QUICKSTART.md | 2 + docs/RUNTIME_HOME.md | 97 ++++++++++++ docs/index.md | 1 + docs/rituals/release.md | 9 +- docs/workflows/pages_publish_example.md | 4 + specs/04_tasks.md | 10 +- specs/zeroclaw_dual_hw_todo.md | 8 +- test/test_openclaw_sanitizer.py | 31 ++-- test/test_validate_specs.py | 4 +- tools/ai/zeroclaw_stack_up.sh | 12 +- tools/bootstrap_python_env.sh | 86 +++++++++++ tools/github_dispatch_mcp_smoke.py | 21 ++- tools/hw/kicad_cli.sh | 15 +- tools/hw/run_kicad_mcp.sh | 14 +- tools/hw/schops/tests/test_rules_engine.py | 2 + tools/lib/runtime_home.sh | 20 +++ tools/notion_mcp.py | 15 +- tools/notion_mcp_smoke.py | 18 ++- tools/review_local_change_bundle.sh | 99 ++++++++++++ tools/run_github_dispatch_mcp.sh | 6 + tools/run_nexar_mcp.sh | 6 + tools/run_notion_mcp.sh | 6 + tools/test_python.sh | 146 ++++++++++++++++++ tools/validate_specs.py | 31 +++- workflows/compliance-release.json | 6 +- workflows/templates/compliance-release.json | 6 +- 35 files changed, 840 insertions(+), 106 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/LOCAL_CHANGE_BUNDLES_2026-03-08.md create mode 100644 docs/RUNTIME_HOME.md create mode 100755 tools/bootstrap_python_env.sh mode change 100644 => 100755 tools/hw/kicad_cli.sh create mode 100644 tools/lib/runtime_home.sh create mode 100755 tools/review_local_change_bundle.sh create mode 100755 tools/test_python.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1cf99aa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + python-stable: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Bootstrap repo-local venv + run: bash tools/bootstrap_python_env.sh + + - name: Run stable Python suite + run: bash tools/test_python.sh --suite stable diff --git a/.github/workflows/release_signing.yml b/.github/workflows/release_signing.yml index 5074c54..1cff4c9 100644 --- a/.github/workflows/release_signing.yml +++ b/.github/workflows/release_signing.yml @@ -5,6 +5,11 @@ on: tags: - 'v*' workflow_dispatch: + inputs: + release_tag: + description: 'Version tag to publish (for example v1.2.3)' + required: true + type: string permissions: contents: write @@ -15,10 +20,24 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Resolve release tag + id: resolve_tag + env: + INPUT_RELEASE_TAG: ${{ github.event.inputs.release_tag || '' }} + run: | + RELEASE_TAG="${INPUT_RELEASE_TAG:-${GITHUB_REF_NAME:-}}" + case "$RELEASE_TAG" in + v*) ;; + *) + echo "release_tag must start with 'v': $RELEASE_TAG" >&2 + exit 1 + ;; + esac + echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" - name: Build artefacts run: | mkdir -p build - printf 'Kill_LIFE release %s\n' "${GITHUB_REF_NAME:-dev}" > build/artefact.bin + printf 'Kill_LIFE release %s\n' "${{ steps.resolve_tag.outputs.release_tag }}" > build/artefact.bin - name: Sign artefacts (cosign) env: COSIGN_KEY: ${{ secrets.SIGNING_KEY }} @@ -32,6 +51,7 @@ jobs: uses: softprops/action-gh-release@v1 with: files: build/artefact.bin - tag_name: ${{ github.ref_name }} - name: "Release ${{ github.ref_name }}" + tag_name: ${{ steps.resolve_tag.outputs.release_tag }} + target_commitish: ${{ github.sha }} + name: "Release ${{ steps.resolve_tag.outputs.release_tag }}" body: "Release automatisée avec artefacts signés." diff --git a/.gitignore b/.gitignore index 9279d6d..f986d7e 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,7 @@ node_modules/ *.pid docker-compose.override.yml .cad-home/ +.runtime-home/ # Autres outils CI/CD coverage/ diff --git a/README.md b/README.md index 5d46fa9..31e39e1 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,40 @@ cd Kill_LIFE bash install_kill_life.sh ``` +### Bootstrap Python repo-local + +```bash +bash tools/bootstrap_python_env.sh +``` + +Options utiles: +- `--venv-dir /tmp/kill-life-venv` pour verifier le bootstrap sur une machine ou un environnement vierge +- `--reinstall` pour recreer proprement le venv cible + +Le chemin supporte pour le Python du repo est `./.venv/bin/python`. + +### Tests Python repo-local + +```bash +bash tools/test_python.sh +``` + +Ce chemin couvre la suite Python repo-locale stable (`setup_repo`, `mcp_runtime_status`, `openclaw_sanitizer`, `apply_safe_patch`, `validate_specs`, `tools/hw/schops/tests`) sans dependre du `python3` systeme. +Les checks dependants du mirror specs ou des runtimes MCP restent des commandes d'integration separees. + +Options utiles: +- `--suite stable` pour le chemin repo-local supporte par defaut +- `--suite mcp` pour les tests MCP locaux (`notion`, `github-dispatch`, `nexar`) +- `--suite all` pour enchainer les deux +- `--bootstrap` pour creer le venv cible avant de lancer les tests +- `--list` pour afficher exactement les commandes couvertes + +Exemple de verification sur un venv temporaire: + +```bash +bash tools/test_python.sh --bootstrap --venv-dir /tmp/kill-life-venv --suite stable +``` + ### Verifications utiles ```bash @@ -78,10 +112,21 @@ La stack CAD est documentee dans [`deploy/cad/README.md`](deploy/cad/README.md) ## Ecosysteme -- [`crazy_life`](https://github.com/electron-rare/crazy_life) : frontend + backend web/devops -- [`mascarade`](https://github.com/electron-rare/mascarade) : repo compagnon et bridge historique +- [`crazy_life`](https://github.com/electron-rare/crazy_life) : repo canonique web/devops +- [`mascarade`](https://github.com/electron-rare/mascarade) : repo compagnon/orchestration et bridge historique optionnel - [`docs/MASCARADE_BRIDGE.md`](docs/MASCARADE_BRIDGE.md) : articulation locale entre les depots +Contrat multi-repo: +- `crazy_life` publie la surface web/devops et le workflow editor. +- `Kill_LIFE` reste la source de verite pour `workflows/*.json`, le runtime, les evidence packs, le firmware, le CAD et la compliance. +- `mascarade` ne redevient pas la source canonique de release web; le bridge reste un mecanisme de sync seulement. + +## CI et release + +- `.github/workflows/ci.yml` porte le gate repo-local stable: bootstrap Python + `bash tools/test_python.sh --suite stable`. +- `.github/workflows/release_signing.yml` reste le workflow de release versionnee; il attend un tag `v*` ou un `workflow_dispatch` avec `release_tag` explicite. +- GitHub Pages n'est pas un gate canonique pour la release `Crazy Lane`; les workflows Pages de `Kill_LIFE` restent des surfaces secondaires docs/evidence. + ## Licence MIT. Voir [`licenses/MIT.txt`](licenses/MIT.txt). diff --git a/ai-agentic-embedded-base/specs/04_tasks.md b/ai-agentic-embedded-base/specs/04_tasks.md index 6d4eb29..9152e2b 100644 --- a/ai-agentic-embedded-base/specs/04_tasks.md +++ b/ai-agentic-embedded-base/specs/04_tasks.md @@ -117,7 +117,7 @@ Format: - Evidence: message `[budget] message length ... exceeds ...`. - Status: `2026-02-21` done (`ZEROCLAW_WEBHOOK_MAX_CHARS=5`, exit 11). -## Integrations Open WebUI + n8n +## Integrations n8n - [x] T-401 - Ajouter API alerts + autotriage backend - AC: endpoints `/api/alerts`, `/api/alerts/ack`, `/api/pr/autotriage` disponibles. @@ -127,23 +127,17 @@ Format: - AC: panneau Alerts + Integrations status + action `PR autotriage now`. - Evidence: `tools/ai/zeroclaw_stack_up.sh`. -- [x] T-403 - Refactor Pipe Open WebUI en mapping mixte - - AC: pipe expose `zc.router` + modeles dedies et parsing `/run`/`/pr`. - - Evidence: `tools/ai/integrations/openwebui/zeroclaw_orchestrator_pipe.py`. - - [x] T-404 - Ajouter workflow n8n PR autotriage - AC: workflow avec Webhook + Cron fallback + alert/question outputs. - Evidence: `tools/ai/integrations/n8n/zeroclaw_pr_autotriage_workflow.json`. - [x] T-405 - Ajouter provisioning local Docker integrations - - AC: compose + scripts `up/down/status` + import n8n + assist Open WebUI. + - AC: compose + scripts `up/down/status` + import n8n. - Evidence: - - `tools/ai/integrations/docker/docker-compose.openwebui-n8n.yml` - `tools/ai/zeroclaw_integrations_up.sh` - `tools/ai/zeroclaw_integrations_down.sh` - `tools/ai/zeroclaw_integrations_status.sh` - `tools/ai/zeroclaw_integrations_import_n8n.sh` - - `tools/ai/zeroclaw_integrations_openwebui_assist.sh` ## Definition of done diff --git a/ai-agentic-embedded-base/specs/mcp_tasks.md b/ai-agentic-embedded-base/specs/mcp_tasks.md index a6d20ca..5a313b9 100644 --- a/ai-agentic-embedded-base/specs/mcp_tasks.md +++ b/ai-agentic-embedded-base/specs/mcp_tasks.md @@ -53,13 +53,15 @@ Format: - AC: un état `ready / degraded / failed` est visible sans lecture manuelle des logs. - [ ] K-012 — Rejouer la validation host-native sur une machine avec `pcbnew` - - AC: le smoke passe aussi sur le chemin hôte, pas seulement via le fallback conteneur. + - AC: le smoke passe aussi sur le chemin hote, pas seulement via le fallback conteneur. + - Helper pret: `python3 tools/hw/kicad_host_mcp_smoke.py --json --quick` degrade proprement si `pcbnew` est absent. - [x] K-013 — Décider du statut final des micro-serveurs `kicad_kic_ai` - AC: `component_database`, `kicad_tools` et `nexar_api` sont explicitement promus en surfaces auxiliaires supportées. - [ ] K-014 — Valider le mode live de `nexar_api` - - AC: un run avec `NEXAR_TOKEN` confirme le comportement réel et le distingue du mode démo. + - AC: un run avec `NEXAR_TOKEN` confirme le comportement reel et le distingue du mode demo. + - Helper pret: `python3 tools/nexar_mcp_smoke.py --json --live` degrade ou echoue proprement tant que le token ou le mode live manquent. - [x] K-015 — Implémenter le MCP `Notion` - AC: `tools/run_notion_mcp.sh` expose `search_pages`, `read_page`, `append_to_page`, `create_page` sans retirer le bridge HTTP en V1. @@ -72,3 +74,12 @@ Format: - [x] K-018 — Etendre l'observabilite MCP a plusieurs serveurs - AC: `/api/ops/summary` expose un etat agrege et le detail par serveur pour `kicad`, `validate-specs`, `notion` et `github-dispatch`. + +- [x] K-019 — Ajouter un helper de readiness host-native KiCad + - AC: `python3 tools/hw/kicad_host_mcp_smoke.py --json --quick` retourne `ready` ou `degraded` sans ambiguite. + +- [x] K-020 — Ajouter un smoke dedie pour `nexar_api` + - AC: `python3 tools/nexar_mcp_smoke.py --json` distingue mode demo et mode live. + +- [x] K-021 — Ajouter un rapport MCP local synthetique + - AC: `python3 tools/mcp_runtime_status.py --json` agrege les smokes supportes et rend visibles les blocages K-012/K-014. diff --git a/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_todo.md b/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_todo.md index d65e220..0c7b9b1 100644 --- a/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_todo.md +++ b/ai-agentic-embedded-base/specs/zeroclaw_dual_hw_todo.md @@ -58,12 +58,11 @@ Last updated: 2026-02-21 - [x] E-003 - Dashboard live usable for continuous supervision. - [x] E-004 - Prometheus target scrape confirmed on gateway metrics. -## Integrations (Open WebUI + n8n) +## Integrations (n8n) -- [x] I-201 - Add mixed Open WebUI Pipe mapping (`zc.router` + dedicated models). - [x] I-202 - Add n8n PR autotriage workflow (webhook + cron fallback). -- [x] I-203 - Add local docker compose stack for Open WebUI (`3001`) and n8n (`5678`). -- [x] I-204 - Add runtime scripts (`integrations_up/down/status`, `import_n8n`, `openwebui_assist`). +- [x] I-203 - Add local docker compose stack for n8n (`5678`). +- [x] I-204 - Add runtime scripts (`integrations_up/down/status`, `import_n8n`). - [ ] I-205 - Validate end-to-end import + activation on local Docker runtime. - command: `tools/ai/zeroclaw_integrations_up.sh` - command: `tools/ai/zeroclaw_integrations_status.sh` diff --git a/docs/GITHUB_PAGES_SETUP.md b/docs/GITHUB_PAGES_SETUP.md index f3607d4..b538686 100644 --- a/docs/GITHUB_PAGES_SETUP.md +++ b/docs/GITHUB_PAGES_SETUP.md @@ -1,30 +1,28 @@ -# Configuration GitHub Pages +# GitHub Pages Posture -Pour que GitHub Pages serve correctement les artefacts et endpoints JSON : +GitHub Pages is not a canonical release surface for `Crazy Lane`. -1. **Branche à publier** : Configurez GitHub Pages pour utiliser la branche `gh-pages`. -2. **Dossier racine** : Choisissez `/` (racine) comme dossier source. -3. **Vérification** : Après exécution du workflow, vérifiez que les fichiers sont bien présents sur https://electron-rare.github.io/Kill_LIFE/ +Current contract: +- `crazy_life` owns the canonical web/devops release path. +- `Kill_LIFE` keeps runtime, workflow JSON, evidence, firmware, CAD and compliance as source of truth. +- Any Pages publication in `Kill_LIFE` is secondary and should be treated as docs/evidence preview only. -## Procédure +## What is canonical today -- Allez dans **Settings > Pages** du dépôt GitHub. -- Sélectionnez : - - **Source** : `gh-pages` - - **Dossier** : `/` (root) -- Sauvegardez. -- Relancez le workflow Pages valide (`static.yml`) si besoin. +- Release gate: repo-local stable CI + versioned release workflow. +- Stable CI: `.github/workflows/ci.yml` +- Versioned release: `.github/workflows/release_signing.yml` -## Points d’attention -- Le workflow doit créer le dossier `public/` et le publier sur `gh-pages`. -- Les fichiers JSON, endpoints.md, evidence pack doivent être dans `public/`. -- Shields.io doit pointer vers : `https://electron-rare.github.io/Kill_LIFE/docs/badges/.json` +## What Pages means here ---- +If Pages is still enabled for `Kill_LIFE`, use it only for lightweight docs/evidence preview. +Do not use it as a proxy for: +- cockpit release readiness +- workflow editor release readiness +- runtime release gating -> Si le dossier public n’est pas à la racine, adaptez le chemin dans le workflow et la configuration Pages. -> Pour une vérification rapide, utilisez : `curl https://electron-rare.github.io/Kill_LIFE/docs/badges/supply_chain_badge.json` +## Legacy note ---- - -**Astuce** : Pour une publication immédiate, utilisez l’action `workflow_dispatch` dans GitHub Actions. +The repository still contains more than one historical Pages workflow shape. +Until one of them is explicitly simplified or removed, treat Pages as non-blocking +and secondary to the canonical CI/release path above. diff --git a/docs/LOCAL_CHANGE_BUNDLES_2026-03-08.md b/docs/LOCAL_CHANGE_BUNDLES_2026-03-08.md new file mode 100644 index 0000000..8e7884f --- /dev/null +++ b/docs/LOCAL_CHANGE_BUNDLES_2026-03-08.md @@ -0,0 +1,92 @@ +# Local Change Bundles — 2026-03-08 + +But: reduire la derive locale de `Kill_LIFE` en regroupant le worktree en lots revus/publies par sujet. + +## Etat courant + +Le worktree local n'est plus un melange de dizaines de sujets heterogenes. Il se lit maintenant en `2` lots principaux: + +1. `mcp-runtime` +2. `python-local` + +Le helper de revue associe est: + +```bash +bash tools/review_local_change_bundle.sh [status|diff|paths] +``` + +## Lot 1 — `mcp-runtime` + +Objet: + +- `runtime home` local explicite pour les launchers MCP et CAD +- smokes MCP alignes sur les modes d'auth reels +- doc operateur associee + +Fichiers: + +- `.gitignore` +- `ai-agentic-embedded-base/specs/mcp_tasks.md` +- `docs/QUICKSTART.md` +- `docs/index.md` +- `docs/RUNTIME_HOME.md` +- `tools/github_dispatch_mcp_smoke.py` +- `tools/hw/kicad_cli.sh` +- `tools/hw/run_kicad_mcp.sh` +- `tools/lib/runtime_home.sh` +- `tools/notion_mcp.py` +- `tools/notion_mcp_smoke.py` +- `tools/run_github_dispatch_mcp.sh` +- `tools/run_nexar_mcp.sh` +- `tools/run_notion_mcp.sh` + +Revue: + +```bash +bash tools/review_local_change_bundle.sh mcp-runtime status +bash tools/review_local_change_bundle.sh mcp-runtime diff +``` + +## Lot 2 — `python-local` + +Objet: + +- bootstrap Python repo-local stable +- commande de tests repo-locale stable +- harness de tests ajustes pour ce chemin supporte + +Fichiers: + +- `README.md` +- `test/test_openclaw_sanitizer.py` +- `tools/bootstrap_python_env.sh` +- `tools/hw/schops/tests/test_rules_engine.py` +- `tools/test_python.sh` + +Revue: + +```bash +bash tools/review_local_change_bundle.sh python-local status +bash tools/review_local_change_bundle.sh python-local diff +``` + +Validation repo-locale: + +```bash +bash tools/bootstrap_python_env.sh +bash tools/test_python.sh --suite stable +``` + +## Ordre recommande + +1. publier ou reviewer `mcp-runtime` +2. publier ou reviewer `python-local` + +Ce decoupage evite de melanger: + +- la hygiene runtime des launchers MCP +- le contrat operateur Python repo-local + +## Note + +Les tests MCP d'integration (`--suite mcp`) restent un lot de verification distinct du chemin repo-local stable. Ils dependent encore des launchers et des runtimes compagnons, donc ils ne servent pas de gate minimale pour le lot `python-local`. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index c59545e..b57d95f 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -76,6 +76,8 @@ tools/hw/cad_stack.sh pio system info Le workspace monté est `Kill_LIFE` par défaut. +Les wrappers locaux qui lancent des runtimes Python/Node ou des conteneurs remappés utilisent un `HOME` explicite local au repo. La règle est documentée dans [RUNTIME_HOME.md](RUNTIME_HOME.md). + --- ## 🤝 Contribuer diff --git a/docs/RUNTIME_HOME.md b/docs/RUNTIME_HOME.md new file mode 100644 index 0000000..16f3934 --- /dev/null +++ b/docs/RUNTIME_HOME.md @@ -0,0 +1,97 @@ +# Runtime Home + +Ce document fixe la règle pour les launchers locaux `Kill_LIFE` qui exécutent : + +- des outils Python +- des outils Node +- des conteneurs Docker remappés avec `--user uid:gid` + +## Problème visé + +Sans `HOME` explicite, certains runtimes essaient d’utiliser un home implicite comme `/home/`. + +En environnement local ou conteneurisé, cela peut produire des erreurs du type : + +```text +EACCES: permission denied, mkdir '/home/clems' +``` + +Le pattern est typique quand : + +- le process ne tourne pas en root +- le parent de `HOME` n’est pas writable +- le runtime tente d’écrire un cache, une config ou un état local + +## Règle projet + +Tout launcher `Kill_LIFE` qui peut écrire localement doit définir explicitement : + +- `HOME` +- `XDG_CONFIG_HOME` +- `XDG_CACHE_HOME` + +Ces chemins doivent pointer vers un répertoire local au repo, jamais vers `/home/` implicite. + +## Emplacements utilisés + +Deux familles sont utilisées dans le repo : + +- `./.runtime-home/` pour les wrappers host-side génériques +- `./.cad-home/` pour les launchers CAD/EDA + +Ces dossiers sont ignorés par Git. + +## Helper commun + +Le helper shell commun est : + +- [tools/lib/runtime_home.sh](../tools/lib/runtime_home.sh) + +Fonctions exposées : + +- `kill_life_runtime_home_init [base_dir]` +- `kill_life_runtime_home_ensure` + +## Exemple minimal + +```bash +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT_DIR/tools/lib/runtime_home.sh" + +kill_life_runtime_home_init "$ROOT_DIR" "my-tool" +kill_life_runtime_home_ensure + +exec python3 my_tool.py "$@" +``` + +## Cas Docker remappé + +Si un conteneur est lancé avec `--user "$(id -u):$(id -g)"`, il faut aussi propager ces variables : + +```bash +docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e HOME="$HOME" \ + -e XDG_CONFIG_HOME="$XDG_CONFIG_HOME" \ + -e XDG_CACHE_HOME="$XDG_CACHE_HOME" \ + image \ + sh -lc 'mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"; exec tool "$@"' sh "$@" +``` + +## Scripts déjà alignés + +- [tools/hw/kicad_cli.sh](../tools/hw/kicad_cli.sh) +- [tools/hw/run_kicad_mcp.sh](../tools/hw/run_kicad_mcp.sh) +- [tools/run_notion_mcp.sh](../tools/run_notion_mcp.sh) +- [tools/run_github_dispatch_mcp.sh](../tools/run_github_dispatch_mcp.sh) +- [tools/run_nexar_mcp.sh](../tools/run_nexar_mcp.sh) + +## Revue rapide avant merge + +Avant d’ajouter un nouveau launcher : + +- vérifier s’il peut écrire des caches/configs +- ne pas supposer que `HOME` existe déjà +- ne pas dépendre d’un home hôte implicite +- créer explicitement les dossiers runtime nécessaires +- ajouter le répertoire d’état à `.gitignore` si besoin diff --git a/docs/index.md b/docs/index.md index d8b55c3..700b195 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,7 @@ Ce repo est un **socle** : - Installation : `docs/INSTALL.md` - Runbook opérateur : `docs/RUNBOOK.md` - FAQ : `docs/FAQ.md` +- Runtime home : `docs/RUNTIME_HOME.md` - Pont vers le repo compagnon d’exécution : `docs/MASCARADE_BRIDGE.md` ## Workflows professionnels (opérationnels) diff --git a/docs/rituals/release.md b/docs/rituals/release.md index 1a463f9..8fbb5b5 100644 --- a/docs/rituals/release.md +++ b/docs/rituals/release.md @@ -1,14 +1,17 @@ # Rituel — Release ## Pré-requis -- Tous gates CI OK +- Gate repo-local stable OK - Evidence pack complet - Notes de release prêtes +- Tag de release prepare (`v*`) ## Étapes - Bump version -- Tag -- Publier artifacts +- Créer le tag `v*` +- Publier les artefacts via `release_signing.yml` + - chemin canonique: push du tag + - fallback opérateur: `workflow_dispatch` avec `release_tag` explicite - Changelog ## Post-release diff --git a/docs/workflows/pages_publish_example.md b/docs/workflows/pages_publish_example.md index c63809f..f81c821 100644 --- a/docs/workflows/pages_publish_example.md +++ b/docs/workflows/pages_publish_example.md @@ -1,5 +1,9 @@ # Publication automatique des artefacts et endpoints sur GitHub Pages +> Exemple secondaire seulement. Ce flux ne remplace pas le contrat canonique de +> release, qui reste `crazy_life` pour le web/devops et `Kill_LIFE` pour le +> runtime/evidence/versioning. + ## Objectif Rendre accessibles publiquement les endpoints JSON des badges dynamiques et les artefacts CI/CD via GitHub Pages. diff --git a/specs/04_tasks.md b/specs/04_tasks.md index 6d4eb29..9152e2b 100644 --- a/specs/04_tasks.md +++ b/specs/04_tasks.md @@ -117,7 +117,7 @@ Format: - Evidence: message `[budget] message length ... exceeds ...`. - Status: `2026-02-21` done (`ZEROCLAW_WEBHOOK_MAX_CHARS=5`, exit 11). -## Integrations Open WebUI + n8n +## Integrations n8n - [x] T-401 - Ajouter API alerts + autotriage backend - AC: endpoints `/api/alerts`, `/api/alerts/ack`, `/api/pr/autotriage` disponibles. @@ -127,23 +127,17 @@ Format: - AC: panneau Alerts + Integrations status + action `PR autotriage now`. - Evidence: `tools/ai/zeroclaw_stack_up.sh`. -- [x] T-403 - Refactor Pipe Open WebUI en mapping mixte - - AC: pipe expose `zc.router` + modeles dedies et parsing `/run`/`/pr`. - - Evidence: `tools/ai/integrations/openwebui/zeroclaw_orchestrator_pipe.py`. - - [x] T-404 - Ajouter workflow n8n PR autotriage - AC: workflow avec Webhook + Cron fallback + alert/question outputs. - Evidence: `tools/ai/integrations/n8n/zeroclaw_pr_autotriage_workflow.json`. - [x] T-405 - Ajouter provisioning local Docker integrations - - AC: compose + scripts `up/down/status` + import n8n + assist Open WebUI. + - AC: compose + scripts `up/down/status` + import n8n. - Evidence: - - `tools/ai/integrations/docker/docker-compose.openwebui-n8n.yml` - `tools/ai/zeroclaw_integrations_up.sh` - `tools/ai/zeroclaw_integrations_down.sh` - `tools/ai/zeroclaw_integrations_status.sh` - `tools/ai/zeroclaw_integrations_import_n8n.sh` - - `tools/ai/zeroclaw_integrations_openwebui_assist.sh` ## Definition of done diff --git a/specs/zeroclaw_dual_hw_todo.md b/specs/zeroclaw_dual_hw_todo.md index d65e220..f228e17 100644 --- a/specs/zeroclaw_dual_hw_todo.md +++ b/specs/zeroclaw_dual_hw_todo.md @@ -58,13 +58,13 @@ Last updated: 2026-02-21 - [x] E-003 - Dashboard live usable for continuous supervision. - [x] E-004 - Prometheus target scrape confirmed on gateway metrics. -## Integrations (Open WebUI + n8n) +## Integrations (n8n) -- [x] I-201 - Add mixed Open WebUI Pipe mapping (`zc.router` + dedicated models). - [x] I-202 - Add n8n PR autotriage workflow (webhook + cron fallback). -- [x] I-203 - Add local docker compose stack for Open WebUI (`3001`) and n8n (`5678`). -- [x] I-204 - Add runtime scripts (`integrations_up/down/status`, `import_n8n`, `openwebui_assist`). +- [x] I-203 - Add local docker compose stack for n8n (`5678`). +- [x] I-204 - Add runtime scripts (`integrations_up/down/status`, `import_n8n`). - [ ] I-205 - Validate end-to-end import + activation on local Docker runtime. + - status: Deferred (post-stabilization) - command: `tools/ai/zeroclaw_integrations_up.sh` - command: `tools/ai/zeroclaw_integrations_status.sh` - command: `tools/ai/zeroclaw_integrations_import_n8n.sh` diff --git a/test/test_openclaw_sanitizer.py b/test/test_openclaw_sanitizer.py index e6bcc60..f4c0ae8 100644 --- a/test/test_openclaw_sanitizer.py +++ b/test/test_openclaw_sanitizer.py @@ -4,30 +4,33 @@ Test automatisé pour la sanitisation des commentaires OpenClaw. Vérifie que le sanitizer retire les patterns à risque et ne laisse aucun secret, code ou mention. """ import sys +import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools" / "ai")) from sanitize_issue import sanitize_text -def test_sanitize_basic(): - input_text = """ + +class OpenClawSanitizerTests(unittest.TestCase): + def test_sanitize_basic(self): + input_text = """ Hello @user, see #123. ```rm -rf /home/user``` Contact: user@example.com Visit https://evil.com !dangerous command password - """ - sanitized = sanitize_text(input_text) - assert "@user" not in sanitized - assert "#123" not in sanitized - assert "rm -rf" not in sanitized - assert "user@example.com" not in sanitized - assert "https://evil.com" not in sanitized - assert "!dangerous" not in sanitized - assert "password" not in sanitized - assert "secret" not in sanitized - print("Sanitization test passed.") + """ + sanitized = sanitize_text(input_text) + self.assertNotIn("@user", sanitized) + self.assertNotIn("#123", sanitized) + self.assertNotIn("rm -rf", sanitized) + self.assertNotIn("user@example.com", sanitized) + self.assertNotIn("https://evil.com", sanitized) + self.assertNotIn("!dangerous", sanitized) + self.assertNotIn("password", sanitized) + self.assertNotIn("secret", sanitized) + if __name__ == "__main__": - test_sanitize_basic() + unittest.main() diff --git a/test/test_validate_specs.py b/test/test_validate_specs.py index d32a439..a7d8f92 100644 --- a/test/test_validate_specs.py +++ b/test/test_validate_specs.py @@ -48,7 +48,9 @@ class ValidateSpecsTests(unittest.TestCase): self.assertIn("compliance", payload) self.assertIn("rfc2119", payload) self.assertIn("mirror_sync", payload) - self.assertTrue(payload["mirror_sync"]["ok"]) + self.assertTrue(payload["ok"]) + self.assertFalse(payload["require_mirror_sync"]) + self.assertTrue(payload["mirror_sync"]["mirror_exists"]) def test_mcp_server_supports_initialize_and_tools_list(self): proc = subprocess.Popen( diff --git a/tools/ai/zeroclaw_stack_up.sh b/tools/ai/zeroclaw_stack_up.sh index e6b75a2..050867f 100755 --- a/tools/ai/zeroclaw_stack_up.sh +++ b/tools/ai/zeroclaw_stack_up.sh @@ -1036,10 +1036,8 @@ cat >"$INDEX_FILE" </orchestrator.log /prometheus.yml /integrations/README.md - openwebui pipe n8n workflow n8n pr-autotriage workflow - compose openwebui+n8n langgraph runbook autogen runbook /health @@ -1162,7 +1160,6 @@ cat >"$INDEX_FILE" <
(integrations status)
-
@@ -1196,8 +1193,7 @@ cat >"$INDEX_FILE" <1) tools/ai/zeroclaw_stack_up.sh 2) tools/ai/zeroclaw_integrations_up.sh 3) Ouvrir http://127.0.0.1:8788/ (monitoring) -4) Ouvrir http://127.0.0.1:3001/ (Open WebUI) -5) Ouvrir http://127.0.0.1:5678/ (n8n) +4) Ouvrir http://127.0.0.1:5678/ (n8n) @@ -2060,7 +2056,6 @@ cat >"$INDEX_FILE" <"$INDEX_FILE" <&2; usage >&2; exit 2; } + PYTHON_BIN="$1" + ;; + --venv-dir) + shift + [[ $# -gt 0 ]] || { echo "Missing value for --venv-dir" >&2; usage >&2; exit 2; } + VENV_DIR="$1" + ;; + --reinstall) + REINSTALL=true + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then + echo "Python interpreter not found: ${PYTHON_BIN}" >&2 + exit 1 +fi + +if [[ "${REINSTALL}" == true && -d "${VENV_DIR}" ]]; then + echo "[bootstrap-python] removing ${VENV_DIR}" + rm -rf "${VENV_DIR}" +fi + +if [[ ! -x "${VENV_DIR}/bin/python" ]]; then + echo "[bootstrap-python] creating ${VENV_DIR}" + "${PYTHON_BIN}" -m venv "${VENV_DIR}" +else + echo "[bootstrap-python] reusing ${VENV_DIR}" +fi + +if ! "${VENV_DIR}/bin/python" -c "import yaml, jsonschema" >/dev/null 2>&1; then + echo "[bootstrap-python] installing minimal test dependencies" + "${VENV_DIR}/bin/python" -m pip install --upgrade pip setuptools wheel + "${VENV_DIR}/bin/python" -m pip install -r "${REQ_FILE}" + "${VENV_DIR}/bin/python" -m pip install 'jsonschema>=4.21.0' +fi + +echo "[bootstrap-python] python: ${VENV_DIR}/bin/python" +echo "[bootstrap-python] test: bash tools/test_python.sh --venv-dir ${VENV_DIR}" diff --git a/tools/github_dispatch_mcp_smoke.py b/tools/github_dispatch_mcp_smoke.py index 4a5cb1a..9f9e717 100755 --- a/tools/github_dispatch_mcp_smoke.py +++ b/tools/github_dispatch_mcp_smoke.py @@ -22,6 +22,19 @@ ROOT = Path(__file__).resolve().parents[1] SERVER = ROOT / "tools" / "run_github_dispatch_mcp.sh" +def github_dispatch_auth_configured() -> bool: + auth_mode = os.getenv("GITHUB_DISPATCH_AUTH_MODE", "token").strip().lower() + if auth_mode == "app": + return bool( + os.getenv("GITHUB_APP_ID", "").strip() + and os.getenv("GITHUB_APP_PRIVATE_KEY", "").strip() + and os.getenv("GITHUB_APP_INSTALLATION_ID", "").strip() + ) + return bool( + os.getenv("KILL_LIFE_GITHUB_TOKEN") or os.getenv("GITHUB_TOKEN") + ) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--timeout", type=float, default=15.0) @@ -42,9 +55,7 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() - token_configured = bool( - os.getenv("KILL_LIFE_GITHUB_TOKEN") or os.getenv("GITHUB_TOKEN") - ) + token_configured = github_dispatch_auth_configured() proc = spawn_server(["bash", str(SERVER)], ROOT) payload = { "status": "failed", @@ -78,7 +89,7 @@ def main() -> int: else: payload["status"] = "degraded" payload["live_validation"] = "missing_secret" - payload["error"] = "KILL_LIFE_GITHUB_TOKEN or GITHUB_TOKEN missing" + payload["error"] = "GitHub dispatch auth missing" return emit_payload(payload, json_output=args.json) list_result = call_tool( @@ -109,7 +120,7 @@ def main() -> int: payload["checks"].append("dispatch_workflow_missing_secret") payload["status"] = "degraded" payload["live_validation"] = "missing_secret" - payload["error"] = "KILL_LIFE_GITHUB_TOKEN or GITHUB_TOKEN missing" + payload["error"] = "GitHub dispatch auth missing" return emit_payload(payload, json_output=args.json) if not args.live: diff --git a/tools/hw/kicad_cli.sh b/tools/hw/kicad_cli.sh old mode 100644 new mode 100755 index c514c5c..4adcfe1 --- a/tools/hw/kicad_cli.sh +++ b/tools/hw/kicad_cli.sh @@ -9,6 +9,9 @@ set -euo pipefail # KICAD_CLI_BIN: override local path # KICAD_DOCKER_IMAGE: override docker image (default: kicad/kicad:nightly) +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT_DIR/lib/runtime_home.sh" + BIN="${KICAD_CLI_BIN:-}" if [[ -z "$BIN" ]]; then if command -v kicad-cli >/dev/null 2>&1; then @@ -33,5 +36,15 @@ IMG="${KICAD_DOCKER_IMAGE:-kicad/kicad:nightly}" # run as current user to avoid root-owned artifacts UIDGID="$(id -u):$(id -g)" WORKDIR="$(pwd)" +kill_life_runtime_home_init "$WORKDIR" "kicad-cli" "$WORKDIR/.cad-home" +kill_life_runtime_home_ensure -exec docker run --rm -u "$UIDGID" -v "$WORKDIR:$WORKDIR" -w "$WORKDIR" "$IMG" kicad-cli "$@" +exec docker run --rm \ + -u "$UIDGID" \ + -v "$WORKDIR:$WORKDIR" \ + -w "$WORKDIR" \ + -e HOME="$HOME" \ + -e XDG_CONFIG_HOME="$XDG_CONFIG_HOME" \ + -e XDG_CACHE_HOME="$XDG_CACHE_HOME" \ + "$IMG" \ + sh -lc 'set -e; mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"; exec kicad-cli "$@"' sh "$@" diff --git a/tools/hw/run_kicad_mcp.sh b/tools/hw/run_kicad_mcp.sh index 79cff00..d22c004 100755 --- a/tools/hw/run_kicad_mcp.sh +++ b/tools/hw/run_kicad_mcp.sh @@ -2,6 +2,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/tools/lib/runtime_home.sh" COMPOSE_FILE="$ROOT_DIR/deploy/cad/docker-compose.yml" VERBOSE=0 DOCTOR=0 @@ -187,9 +188,13 @@ MASCARADE_DIR="${MASCARADE_DIR:-$REPO_PARENT/mascarade}" SERVER_DIR="$MASCARADE_DIR/finetune/kicad_mcp_server" ENTRYPOINT="${KICAD_MCP_ENTRYPOINT:-$SERVER_DIR/dist/index.js}" NODE_BIN="${NODE_BIN:-node}" -MCP_HOME="${KICAD_MCP_HOME:-$ROOT_DIR/.cad-home/kicad-mcp}" -CONFIG_HOME="${XDG_CONFIG_HOME:-$MCP_HOME/.config}" -CACHE_HOME="${XDG_CACHE_HOME:-$MCP_HOME/.cache}" +if [ -n "${KICAD_MCP_HOME:-}" ] && [ -z "${KILL_LIFE_RUNTIME_HOME:-}" ]; then + export KILL_LIFE_RUNTIME_HOME="$KICAD_MCP_HOME" +fi +kill_life_runtime_home_init "$ROOT_DIR" "kicad-mcp" "$ROOT_DIR/.cad-home" +MCP_HOME="$HOME" +CONFIG_HOME="$XDG_CONFIG_HOME" +CACHE_HOME="$XDG_CACHE_HOME" DATA_DIR="${KICAD_MCP_DATA_DIR:-$MCP_HOME/data}" PYTHONPATH_VALUE="$(build_pythonpath)" PROBE_PYTHON="$(resolve_probe_python)" @@ -248,7 +253,8 @@ fi [ -d "$MASCARADE_DIR" ] || die "companion repo not found: $MASCARADE_DIR" [ -n "$PROBE_PYTHON" ] || die "no Python executable found for KiCad MCP runtime" -mkdir -p "$MCP_HOME" "$CONFIG_HOME" "$CACHE_HOME" "$DATA_DIR" +kill_life_runtime_home_ensure +mkdir -p "$DATA_DIR" export HOME="$MCP_HOME" export XDG_CONFIG_HOME="$CONFIG_HOME" diff --git a/tools/hw/schops/tests/test_rules_engine.py b/tools/hw/schops/tests/test_rules_engine.py index 6524b24..bd45e74 100644 --- a/tools/hw/schops/tests/test_rules_engine.py +++ b/tools/hw/schops/tests/test_rules_engine.py @@ -1,4 +1,5 @@ import importlib.util +import sys from pathlib import Path import unittest @@ -8,6 +9,7 @@ def load_schops_module(): spec = importlib.util.spec_from_file_location("schops", schops_path) assert spec and spec.loader mod = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = mod spec.loader.exec_module(mod) # type: ignore return mod diff --git a/tools/lib/runtime_home.sh b/tools/lib/runtime_home.sh new file mode 100644 index 0000000..26fe765 --- /dev/null +++ b/tools/lib/runtime_home.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# shellcheck shell=bash + +kill_life_runtime_home_init() { + local root_dir="$1" + local runtime_name="$2" + local base_dir="${3:-$root_dir/.runtime-home}" + + RUNTIME_HOME="${KILL_LIFE_RUNTIME_HOME:-$base_dir/$runtime_name}" + XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$RUNTIME_HOME/.config}" + XDG_CACHE_HOME="${XDG_CACHE_HOME:-$RUNTIME_HOME/.cache}" + HOME="$RUNTIME_HOME" + + export RUNTIME_HOME HOME XDG_CONFIG_HOME XDG_CACHE_HOME +} + +kill_life_runtime_home_ensure() { + mkdir -p "$RUNTIME_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" +} diff --git a/tools/notion_mcp.py b/tools/notion_mcp.py index 2b25ae3..0ff8d90 100755 --- a/tools/notion_mcp.py +++ b/tools/notion_mcp.py @@ -26,8 +26,10 @@ MASCARADE_CORE_DIR = MASCARADE_DIR / "core" if str(MASCARADE_CORE_DIR) not in sys.path: sys.path.insert(0, str(MASCARADE_CORE_DIR)) -from mascarade.config import is_secret_configured, settings # noqa: E402 -from mascarade.integrations.notion import NotionClient # noqa: E402 +from mascarade.integrations.notion import ( # noqa: E402 + NotionClient, + notion_auth_configured, +) TOOLS = [ @@ -95,15 +97,18 @@ def _missing_secret_payload() -> dict[str, Any]: "ok": False, "error": { "code": "missing_secret", - "message": "Notion non configure (NOTION_API_KEY manquant)", + "message": ( + "Notion non configure " + "(NOTION_API_KEY ou credentials OAuth Notion manquants)" + ), }, } async def _with_client(callback): - if not is_secret_configured(settings.notion_api_key): + if not notion_auth_configured(): return error_tool_result( - "Notion non configure (NOTION_API_KEY manquant)", + "Notion non configure (NOTION_API_KEY ou credentials OAuth Notion manquants)", _missing_secret_payload(), ) diff --git a/tools/notion_mcp_smoke.py b/tools/notion_mcp_smoke.py index 715fe7c..135a4de 100755 --- a/tools/notion_mcp_smoke.py +++ b/tools/notion_mcp_smoke.py @@ -23,6 +23,20 @@ SERVER = ROOT / "tools" / "run_notion_mcp.sh" PAGE_ID_ENV = "NOTION_MCP_SMOKE_PAGE_ID" +def notion_auth_configured() -> bool: + auth_mode = os.getenv("NOTION_AUTH_MODE", "api_key").strip().lower() + if auth_mode == "oauth_oidc": + return bool( + os.getenv("NOTION_OAUTH_CLIENT_ID", "").strip() + and os.getenv("NOTION_OAUTH_CLIENT_SECRET", "").strip() + and ( + os.getenv("NOTION_OAUTH_ACCESS_TOKEN", "").strip() + or os.getenv("NOTION_OAUTH_REFRESH_TOKEN", "").strip() + ) + ) + return bool(os.getenv("NOTION_API_KEY")) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--timeout", type=float, default=15.0) @@ -34,7 +48,7 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() - secret_configured = bool(os.getenv("NOTION_API_KEY")) + secret_configured = notion_auth_configured() proc = spawn_server(["bash", str(SERVER)], ROOT) payload = { "status": "failed", @@ -67,7 +81,7 @@ def main() -> int: else: payload["status"] = "degraded" payload["live_validation"] = "missing_secret" - payload["error"] = "NOTION_API_KEY missing" + payload["error"] = "NOTION auth missing" return emit_payload(payload, json_output=args.json) search_result = call_tool( diff --git a/tools/review_local_change_bundle.sh b/tools/review_local_change_bundle.sh new file mode 100755 index 0000000..ccd663c --- /dev/null +++ b/tools/review_local_change_bundle.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MODE="status" +BUNDLE="" + +usage() { + cat <<'EOF' +Usage: bash tools/review_local_change_bundle.sh [mode] + +Bundles: + mcp-runtime Runtime-home, MCP launchers, MCP smokes, docs MCP associees + python-local Bootstrap/test Python repo-local et harness associes + all Ensemble des lots suivis localement + +Modes: + status git status cible sur le bundle (default) + diff git diff cible sur le bundle + paths liste exacte des fichiers du bundle + +Examples: + bash tools/review_local_change_bundle.sh mcp-runtime + bash tools/review_local_change_bundle.sh python-local diff + bash tools/review_local_change_bundle.sh all paths +EOF +} + +bundle_paths() { + case "$1" in + mcp-runtime) + cat <<'EOF' +.gitignore +ai-agentic-embedded-base/specs/mcp_tasks.md +docs/QUICKSTART.md +docs/index.md +docs/RUNTIME_HOME.md +tools/github_dispatch_mcp_smoke.py +tools/hw/kicad_cli.sh +tools/hw/run_kicad_mcp.sh +tools/lib/runtime_home.sh +tools/notion_mcp.py +tools/notion_mcp_smoke.py +tools/run_github_dispatch_mcp.sh +tools/run_nexar_mcp.sh +tools/run_notion_mcp.sh +EOF + ;; + python-local) + cat <<'EOF' +README.md +test/test_openclaw_sanitizer.py +tools/bootstrap_python_env.sh +tools/hw/schops/tests/test_rules_engine.py +tools/test_python.sh +EOF + ;; + all) + { + bundle_paths mcp-runtime + bundle_paths python-local + } | awk '!seen[$0]++' + ;; + *) + echo "Unknown bundle: $1" >&2 + usage >&2 + exit 2 + ;; + esac +} + +if [[ $# -lt 1 || $# -gt 2 ]]; then + usage >&2 + exit 2 +fi + +BUNDLE="$1" +if [[ $# -eq 2 ]]; then + MODE="$2" +fi + +mapfile -t PATHS < <(bundle_paths "$BUNDLE") + +case "$MODE" in + status) + exec git -C "$ROOT_DIR" status --short -- "${PATHS[@]}" + ;; + diff) + exec git -C "$ROOT_DIR" diff -- "${PATHS[@]}" + ;; + paths) + printf '%s\n' "${PATHS[@]}" + ;; + *) + echo "Unknown mode: $MODE" >&2 + usage >&2 + exit 2 + ;; +esac diff --git a/tools/run_github_dispatch_mcp.sh b/tools/run_github_dispatch_mcp.sh index ee7330c..33fff85 100755 --- a/tools/run_github_dispatch_mcp.sh +++ b/tools/run_github_dispatch_mcp.sh @@ -4,6 +4,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" MASCARADE_DIR="${MASCARADE_DIR:-$ROOT_DIR/../mascarade}" SERVER_SCRIPT="$ROOT_DIR/tools/github_dispatch_mcp.py" +source "$ROOT_DIR/tools/lib/runtime_home.sh" +kill_life_runtime_home_init "$ROOT_DIR" "github-dispatch-mcp" detect_core_python() { local candidate="${MASCARADE_CORE_PYTHON:-}" @@ -36,6 +38,9 @@ ROOT_DIR=$ROOT_DIR MASCARADE_DIR=$MASCARADE_DIR CORE_PYTHON=$CORE_PYTHON SERVER_SCRIPT=$SERVER_SCRIPT +HOME=$RUNTIME_HOME +XDG_CONFIG_HOME=$XDG_CONFIG_HOME +XDG_CACHE_HOME=$XDG_CACHE_HOME EOF exit 0 fi @@ -45,5 +50,6 @@ fi exit 1 } +kill_life_runtime_home_ensure export PYTHONPATH="$MASCARADE_DIR/core${PYTHONPATH:+:$PYTHONPATH}" exec "$CORE_PYTHON" "$SERVER_SCRIPT" "$@" diff --git a/tools/run_nexar_mcp.sh b/tools/run_nexar_mcp.sh index 1136ff2..dcfb102 100755 --- a/tools/run_nexar_mcp.sh +++ b/tools/run_nexar_mcp.sh @@ -6,6 +6,8 @@ MASCARADE_DIR="${MASCARADE_DIR:-$ROOT_DIR/../mascarade}" KICAD_KIC_AI_DIR="${KICAD_KIC_AI_DIR:-$MASCARADE_DIR/finetune/kicad_kic_ai}" PYTHON_BIN="${NEXAR_MCP_PYTHON:-}" TOKEN_CONFIGURED="no" +source "$ROOT_DIR/tools/lib/runtime_home.sh" +kill_life_runtime_home_init "$ROOT_DIR" "nexar-mcp" if [ -n "${NEXAR_TOKEN:-${NEXAR_API_KEY:-}}" ]; then TOKEN_CONFIGURED="yes" @@ -27,6 +29,9 @@ KICAD_KIC_AI_DIR=$KICAD_KIC_AI_DIR NEXAR_MCP_PYTHON=$PYTHON_BIN SERVER_MODULE=mcp_servers.nexar NEXAR_TOKEN_CONFIGURED=$TOKEN_CONFIGURED +HOME=$RUNTIME_HOME +XDG_CONFIG_HOME=$XDG_CONFIG_HOME +XDG_CACHE_HOME=$XDG_CACHE_HOME EOF exit 0 fi @@ -41,6 +46,7 @@ fi exit 1 } +kill_life_runtime_home_ensure if [ -n "${PYTHONPATH:-}" ]; then export PYTHONPATH="$KICAD_KIC_AI_DIR:$PYTHONPATH" else diff --git a/tools/run_notion_mcp.sh b/tools/run_notion_mcp.sh index 6be04bf..5a110c6 100755 --- a/tools/run_notion_mcp.sh +++ b/tools/run_notion_mcp.sh @@ -4,6 +4,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" MASCARADE_DIR="${MASCARADE_DIR:-$ROOT_DIR/../mascarade}" SERVER_SCRIPT="$ROOT_DIR/tools/notion_mcp.py" +source "$ROOT_DIR/tools/lib/runtime_home.sh" +kill_life_runtime_home_init "$ROOT_DIR" "notion-mcp" detect_core_python() { local candidate="${MASCARADE_CORE_PYTHON:-}" @@ -36,6 +38,9 @@ ROOT_DIR=$ROOT_DIR MASCARADE_DIR=$MASCARADE_DIR CORE_PYTHON=$CORE_PYTHON SERVER_SCRIPT=$SERVER_SCRIPT +HOME=$RUNTIME_HOME +XDG_CONFIG_HOME=$XDG_CONFIG_HOME +XDG_CACHE_HOME=$XDG_CACHE_HOME EOF exit 0 fi @@ -45,5 +50,6 @@ fi exit 1 } +kill_life_runtime_home_ensure export PYTHONPATH="$MASCARADE_DIR/core${PYTHONPATH:+:$PYTHONPATH}" exec "$CORE_PYTHON" "$SERVER_SCRIPT" "$@" diff --git a/tools/test_python.sh b/tools/test_python.sh new file mode 100755 index 0000000..a5eaab0 --- /dev/null +++ b/tools/test_python.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENV_DIR="${ROOT_DIR}/.venv" +PYTHON_BIN="${PYTHON_BIN:-python3}" +BOOTSTRAP=false +SUITE="stable" +LIST_ONLY=false + +usage() { + cat <<'EOF' +Usage: bash tools/test_python.sh [options] + +Run the supported Kill_LIFE repo-local Python suites through the repo-local venv. + +Options: + --suite NAME stable | mcp | all (default: stable) + --bootstrap Create/install the venv first if missing + --python BIN Python interpreter forwarded to bootstrap if needed + --venv-dir PATH Virtualenv directory to use (default: ./.venv) + --list Print the covered commands and exit + -h, --help Show this help + +Suites: + stable Repo-local unittest suite without companion runtimes. + mcp MCP integration tests that exercise local launchers. + all stable + mcp. +EOF +} + +run_discover() { + local start_dir="$1" + local pattern="$2" + ( + cd "${ROOT_DIR}" + "${VENV_DIR}/bin/python" -m unittest discover -s "${start_dir}" -p "${pattern}" + ) +} + +print_suite() { + case "$1" in + stable) + cat <<'EOF' +stable: + test/test_setup_repo_dry_run.py + test/test_mcp_runtime_status.py + test/test_openclaw_sanitizer.py + test/test_apply_safe_patch.py + test/test_validate_specs.py + tools/hw/schops/tests/test_*.py +EOF + ;; + mcp) + cat <<'EOF' +mcp: + test/test_notion_mcp.py + test/test_github_dispatch_mcp.py + test/test_nexar_mcp.py +EOF + ;; + all) + print_suite stable + print_suite mcp + ;; + *) + echo "Unknown suite: $1" >&2 + return 2 + ;; + esac +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --suite) + shift + [[ $# -gt 0 ]] || { echo "Missing value for --suite" >&2; usage >&2; exit 2; } + SUITE="$1" + ;; + --bootstrap) + BOOTSTRAP=true + ;; + --python) + shift + [[ $# -gt 0 ]] || { echo "Missing value for --python" >&2; usage >&2; exit 2; } + PYTHON_BIN="$1" + ;; + --venv-dir) + shift + [[ $# -gt 0 ]] || { echo "Missing value for --venv-dir" >&2; usage >&2; exit 2; } + VENV_DIR="$1" + ;; + --list) + LIST_ONLY=true + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +case "${SUITE}" in + stable|mcp|all) + ;; + *) + echo "Unknown suite: ${SUITE}" >&2 + usage >&2 + exit 2 + ;; +esac + +if [[ "${LIST_ONLY}" == true ]]; then + print_suite "${SUITE}" + exit 0 +fi + +if [[ "${BOOTSTRAP}" == true ]]; then + bash "${ROOT_DIR}/tools/bootstrap_python_env.sh" --python "${PYTHON_BIN}" --venv-dir "${VENV_DIR}" +elif [[ ! -x "${VENV_DIR}/bin/python" ]]; then + echo "Missing ${VENV_DIR}. Run: bash tools/bootstrap_python_env.sh --venv-dir ${VENV_DIR}" >&2 + exit 1 +fi + +export PYTHONPATH="${ROOT_DIR}${PYTHONPATH:+:${PYTHONPATH}}" + +if [[ "${SUITE}" == "stable" || "${SUITE}" == "all" ]]; then + run_discover test 'test_setup_repo_dry_run.py' + run_discover test 'test_mcp_runtime_status.py' + run_discover test 'test_openclaw_sanitizer.py' + run_discover test 'test_apply_safe_patch.py' + run_discover test 'test_validate_specs.py' + run_discover tools/hw/schops/tests 'test_*.py' +fi + +if [[ "${SUITE}" == "mcp" || "${SUITE}" == "all" ]]; then + run_discover test 'test_notion_mcp.py' + run_discover test 'test_github_dispatch_mcp.py' + run_discover test 'test_nexar_mcp.py' +fi diff --git a/tools/validate_specs.py b/tools/validate_specs.py index af26222..27f7f14 100644 --- a/tools/validate_specs.py +++ b/tools/validate_specs.py @@ -100,7 +100,9 @@ def compare_spec_mirror() -> Dict[str, Any]: } -def validate_specs(strict: bool = False) -> Dict[str, Any]: +def validate_specs( + strict: bool = False, require_mirror_sync: bool = False +) -> Dict[str, Any]: required_files = [ "specs/03_plan.md", "specs/04_tasks.md", @@ -116,11 +118,17 @@ def validate_specs(strict: bool = False) -> Dict[str, Any]: rfc2119 = scan_rfc2119() mirror_sync = compare_spec_mirror() - ok = not missing_files and compliance["ok"] and rfc2119["ok"] and mirror_sync["ok"] + ok = ( + not missing_files + and compliance["ok"] + and rfc2119["ok"] + and (mirror_sync["ok"] or not require_mirror_sync) + ) return { "ok": ok, "missing_files": missing_files, "strict": strict, + "require_mirror_sync": require_mirror_sync, "compliance": compliance, "rfc2119": rfc2119, "mirror_sync": mirror_sync, @@ -169,7 +177,10 @@ def format_cli_summary(result: Dict[str, Any]) -> str: def tool_validate_specs(arguments: Dict[str, Any]) -> Dict[str, Any]: strict = bool(arguments.get("strict", False)) - result = validate_specs(strict=strict) + require_mirror_sync = bool(arguments.get("require_mirror_sync", False)) + result = validate_specs( + strict=strict, require_mirror_sync=require_mirror_sync + ) return { "content": [{"type": "text", "text": format_cli_summary(result)}], "structuredContent": result, @@ -204,6 +215,11 @@ TOOLS = [ "type": "boolean", "description": "Enable strict evidence checks from tools/compliance/validate.py.", "default": False, + }, + "require_mirror_sync": { + "type": "boolean", + "description": "Fail if ai-agentic-embedded-base/specs is not synced with specs/.", + "default": False, } }, }, @@ -312,6 +328,11 @@ def serve_mcp() -> int: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Validate Kill_LIFE specs") parser.add_argument("--strict", action="store_true", help="Enable strict compliance evidence validation.") + parser.add_argument( + "--require-mirror-sync", + action="store_true", + help="Fail if ai-agentic-embedded-base/specs is not synchronized with specs/.", + ) parser.add_argument("--json", action="store_true", help="Print JSON output in CLI mode.") parser.add_argument("--mcp", action="store_true", help="Run as an MCP stdio server.") return parser.parse_args() @@ -322,7 +343,9 @@ def main() -> int: if args.mcp: return serve_mcp() - result = validate_specs(strict=args.strict) + result = validate_specs( + strict=args.strict, require_mirror_sync=args.require_mirror_sync + ) if args.json: print(json.dumps(result, indent=2, ensure_ascii=False)) else: diff --git a/workflows/compliance-release.json b/workflows/compliance-release.json index 1f526b8..1de5cd2 100644 --- a/workflows/compliance-release.json +++ b/workflows/compliance-release.json @@ -71,7 +71,8 @@ "checklist": [ "Release notes drafted", "Evidence reviewed", - "Branch protections green" + "Branch protections green", + "Release tag chosen" ] } }, @@ -87,7 +88,8 @@ "workflow_file": "release_signing.yml", "ref": "main", "inputs": { - "source": "mascarade-kill-life-editor" + "source": "crazy-life-kill-life-editor", + "release_tag": "v0.0.0-manual" } }, "config": {} diff --git a/workflows/templates/compliance-release.json b/workflows/templates/compliance-release.json index a6507dd..1de5cd2 100644 --- a/workflows/templates/compliance-release.json +++ b/workflows/templates/compliance-release.json @@ -71,7 +71,8 @@ "checklist": [ "Release notes drafted", "Evidence reviewed", - "Branch protections green" + "Branch protections green", + "Release tag chosen" ] } }, @@ -87,7 +88,8 @@ "workflow_file": "release_signing.yml", "ref": "main", "inputs": { - "source": "crazy-life-kill-life-editor" + "source": "crazy-life-kill-life-editor", + "release_tag": "v0.0.0-manual" } }, "config": {}