feat: integrate local CAD stack and runtime safeguards

This commit is contained in:
Clément SAILLANT
2026-03-08 03:28:31 +01:00
parent 87a2e84c22
commit 46e1feba84
35 changed files with 840 additions and 106 deletions
+29
View File
@@ -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
+23 -3
View File
@@ -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."
+1
View File
@@ -91,6 +91,7 @@ node_modules/
*.pid
docker-compose.override.yml
.cad-home/
.runtime-home/
# Autres outils CI/CD
coverage/
+47 -2
View File
@@ -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).
+2 -8
View File
@@ -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
+13 -2
View File
@@ -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.
@@ -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`
+20 -22
View File
@@ -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 dattention
- 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/<badge>.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 nest 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 laction `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.
+92
View File
@@ -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 <bundle> [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`.
+2
View File
@@ -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
+97
View File
@@ -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 dutiliser un home implicite comme `/home/<user>`.
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` nest 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/<user>` implicite.
## Emplacements utilisés
Deux familles sont utilisées dans le repo :
- `./.runtime-home/<tool>` pour les wrappers host-side génériques
- `./.cad-home/<tool>` 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 <root_dir> <runtime_name> [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 dajouter un nouveau launcher :
- vérifier sil peut écrire des caches/configs
- ne pas supposer que `HOME` existe déjà
- ne pas dépendre dun home hôte implicite
- créer explicitement les dossiers runtime nécessaires
- ajouter le répertoire d’état à `.gitignore` si besoin
+1
View File
@@ -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 dexécution : `docs/MASCARADE_BRIDGE.md`
## Workflows professionnels (opérationnels)
+6 -3
View File
@@ -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
+4
View File
@@ -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.
+2 -8
View File
@@ -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
+4 -4
View File
@@ -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`
+17 -14
View File
@@ -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
<secret>password</secret>
"""
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()
+3 -1
View File
@@ -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(
+2 -10
View File
@@ -1036,10 +1036,8 @@ cat >"$INDEX_FILE" <<EOF
<a href="/orchestrator.log">/orchestrator.log</a>
<a href="/prometheus.yml">/prometheus.yml</a>
<a href="/integrations/README.md">/integrations/README.md</a>
<a href="/integrations/openwebui/zeroclaw_orchestrator_pipe.py">openwebui pipe</a>
<a href="/integrations/n8n/zeroclaw_orchestrator_workflow.json">n8n workflow</a>
<a href="/integrations/n8n/zeroclaw_pr_autotriage_workflow.json">n8n pr-autotriage workflow</a>
<a href="/integrations/docker/docker-compose.openwebui-n8n.yml">compose openwebui+n8n</a>
<a href="/integrations/langgraph/README.md">langgraph runbook</a>
<a href="/integrations/autogen/README.md">autogen runbook</a>
<a href="http://$GATEWAY_HOST:$GATEWAY_PORT/health">/health</a>
@@ -1162,7 +1160,6 @@ cat >"$INDEX_FILE" <<EOF
<div class="body">
<pre id="integrationsStatus">(integrations status)</pre>
<div class="btn-row">
<button onclick="window.open('http://127.0.0.1:3001','_blank')">Open WebUI</button>
<button onclick="window.open('http://127.0.0.1:5678','_blank')">Open n8n</button>
</div>
</div>
@@ -1196,8 +1193,7 @@ cat >"$INDEX_FILE" <<EOF
<pre id="hubQuickStart">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)</pre>
4) Ouvrir http://127.0.0.1:5678/ (n8n)</pre>
</div>
</section>
</div>
@@ -2060,7 +2056,6 @@ cat >"$INDEX_FILE" <<EOF
" paired=" + boolText(health.paired) +
" prom_ok=" + boolText(health.prometheus_ok) +
" prom_status=" + safeText(health.prometheus_status) +
" openwebui_ok=" + boolText(health.openwebui_ok) +
" n8n_ok=" + boolText(health.n8n_ok) +
"\nactive_alerts: " + safeText(alerts.active_count || 0) +
"\nlast_convo: " + safeText(last.conversation || "(none)") +
@@ -2068,10 +2063,7 @@ cat >"$INDEX_FILE" <<EOF
document.getElementById("opsStatus").textContent = ops;
const integrationsText =
"openwebui_url=" + safeText(integrations.openwebui_url || "http://127.0.0.1:3001") +
" status=" + safeText(health.openwebui_status) +
" ok=" + boolText(health.openwebui_ok) +
"\nn8n_url=" + safeText(integrations.n8n_url || "http://127.0.0.1:5678") +
"n8n_url=" + safeText(integrations.n8n_url || "http://127.0.0.1:5678") +
" status=" + safeText(health.n8n_status) +
" ok=" + boolText(health.n8n_ok);
document.getElementById("integrationsStatus").textContent = integrationsText;
+86
View File
@@ -0,0 +1,86 @@
#!/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}"
REINSTALL=false
REQ_FILE="${ROOT_DIR}/tools/compliance/requirements.txt"
usage() {
cat <<'EOF'
Usage: bash tools/bootstrap_python_env.sh [options]
Create or refresh the supported repo-local Python environment for Kill_LIFE.
Options:
--python BIN Python interpreter to use (default: PYTHON_BIN or python3)
--venv-dir PATH Virtualenv directory to create/reuse (default: ./.venv)
--reinstall Remove and recreate the target virtualenv before install
-h, --help Show this help
Notes:
- The stable repo-local suite uses unittest and a minimal dependency set.
- PyYAML is installed because tools/validate_specs.py depends on compliance YAML helpers.
- jsonschema is installed because test_apply_safe_patch exercises tools/mistral/apply_safe_patch.py.
Examples:
bash tools/bootstrap_python_env.sh
bash tools/bootstrap_python_env.sh --venv-dir /tmp/kill-life-venv --reinstall
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--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"
;;
--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}"
+16 -5
View File
@@ -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:
Regular → Executable
+14 -1
View File
@@ -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 "$@"
+10 -4
View File
@@ -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"
@@ -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
+20
View File
@@ -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"
}
+10 -5
View File
@@ -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(),
)
+16 -2
View File
@@ -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(
+99
View File
@@ -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 <bundle> [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
+6
View File
@@ -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" "$@"
+6
View File
@@ -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
+6
View File
@@ -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" "$@"
+146
View File
@@ -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
+27 -4
View File
@@ -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:
+4 -2
View File
@@ -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": {}
+4 -2
View File
@@ -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": {}