Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ Last updated: 2026-07-19

## Backlog

- [ ] **Confirmation E2E (optionnel)** — vraie invite auth-gateway pour un nouvel utilisateur → `whoami` renvoie le workspace. Le trigger + backfill Yani sont appliqués et vérifiés en prod (test comportemental rollback : fire sur UPDATE-après-INSERT + no-op sur company_id inchangé). Follow-ups ouverts : #62 (M2 cast uuid), #63 (M5 checklist), #64 (L3 doc CLAUDE.md). Décision edifice hors périmètre : trigger companion pour `edifice_profiles.organization_id` (Steeve).
- [ ] **Confirmation E2E (optionnel)** — vraie invite auth-gateway pour un nouvel utilisateur → `whoami` renvoie le workspace. Le trigger + backfill Yani sont appliqués et vérifiés en prod (test comportemental rollback : fire sur UPDATE-après-INSERT + no-op sur company_id inchangé). Follow-ups ouverts : #63 (M5 checklist), #64 (L3 doc CLAUDE.md) — **#61 (M1 UNIQUE company_id) et #62 (M2 cast uuid) faits, voir Done ci-dessous**. Décision edifice hors périmètre : trigger companion pour `edifice_profiles.organization_id` (Steeve). ⚠️ **Nouveau follow-up edifice** : `trg_provision_edifice_org` porte le même cast `::uuid` non gardé → un `company_id` malformé abortera l'invite pour tout user `allowed_apps:edifice` ; issue miroir de #62 à ouvrir côté edifice.

- [ ] **Nettoyage local hal (à faire par Renaud, `rm` requis)** — untracked à supprimer : 3 stubs superseded `docs/features/ops-phase-{1,2,3}-*.md` (« Safe to delete »), `benchmark/` (archivé au tag `archive/benchmark-2026-06`), `workspaces/` (legacy), `tmp.txt` (scratch). Non touchés dans cette session (permissions `rm` bloquées + dirs non créés par l'agent). `ops-phase-4-docx-geodata-design.md` = **à garder** (design note verrouillé).
- [ ] **Ops track — 11 runs Archon, 1 brief par repo** — runbook maître `docs/features/ops-runbook.md` (séquencement, commandes de lancement, étapes manuelles). Phase 1 purge+CI : `ops-1-purge-ci.md` dans hal, edifice, bluegreen-marketplace, renaud-marketplace, auth-gateway. Phase 2 : hal `ops-2-deploy-make.md`, bgm `ops-2-release-script.md`, rm `ops-2-release-improve-map.md` + `ops-2-briefing-headless.md`, ag `ops-2-deploy-script.md`. Phase 3 : `ops-3-night-shift.md` (copié dans bluegreen-ops au bootstrap). Les 3 fichiers `ops-phase-*.md` sont des stubs "superseded" à supprimer au commit.

## Done (current sprint)
- [x] **#62 — guard `::uuid` cast dans le trigger de provisioning — 2026-07-22** — migration `20260722195616_guard_company_id_uuid_cast.sql` (fix-forward, trigger-only : le `20260719000000` est déjà en prod). Le `WHEN` de `trg_provision_workspace_membership` valide désormais le format UUID canonique par regex `~*` avant le cast — une valeur malformée fait échouer `WHEN`, la fonction ne s'exécute jamais, l'écriture `auth.users` commit (skip silencieux). Corps de fonction inchangé (pas de `CREATE OR REPLACE`), `WHEN` ne lit que `NEW` (pas de régression `42P17`). **Appliqué en prod `zgkvbjqlvebttbnkklpo` via `apply_migration`** + test comportemental rollback (allowed_apps strippé pour isoler ce trigger) : malformé → commit sans `22P02`, 0 membership ; UUID valide → provisionne (6→7) ; 0 résidu. ⚠️ Découverte : `trg_provision_edifice_org` (edifice) a le même bug non gardé → invite toujours non protégée pour `allowed_apps:edifice` tant qu'edifice n'a pas le guard miroir (issue edifice à ouvrir).
- [x] **#61 (M1) — `UNIQUE (company_id)` sur `halcrm_workspaces` — 2026-07-22** — migration `20260722202028_halcrm_workspaces_company_id_unique.sql` : `ADD CONSTRAINT halcrm_workspaces_company_id_key UNIQUE (company_id)`. Fait tenir par construction l'invariant 1:1 `company_id → halcrm_workspaces` que `provision_workspace_membership()` suppose déjà (son `SELECT … WHERE w.company_id` doit résoudre ≤ 1 workspace, sinon 2 inserts `workspace_members` calculent `is_default=true` dans la même instruction → collision sur `workspace_members_one_default_per_user` → abort du write `auth.users`). `company_id` reste nullable (UNIQUE Postgres autorise plusieurs NULL → pas de backfill). **Appliqué en prod `zgkvbjqlvebttbnkklpo` via `apply_migration`** (0 doublon : null, 26a1de94, 8323074d). Vérifié : `pg_constraint` OK, 3 lignes intactes, **test comportemental rollback** (insert d'un 2e workspace réutilisant un `company_id` existant → `unique_violation` à l'INSERT `halcrm_workspaces` lui-même, fort et au bon endroit). Aucune modif `index.ts` (aucun chemin hal-mcp ne touche l'unicité `company_id`). Reste #62/#63/#64.
- [x] **`workspace_members` auto-provisioning trigger — PR #60 mergée — 2026-07-19** — brief `docs/features/workspace-members-provisioning-trigger.md` implémenté via Archon `archon-idea-to-pr` (worktree isolé). Migration `20260719000000_provision_workspace_membership.sql` : fonction `SECURITY DEFINER` résolvant le workspace par `company_id` joint sur `halcrm_workspaces`, `is_default` **calculé** (garde l'index partiel `workspace_members_one_default_per_user`), `ON CONFLICT DO NOTHING` ; trigger `AFTER INSERT OR UPDATE OF raw_app_meta_data` WHEN-gated (company_id non-null ET changé) ; backfill one-shot Yani. **Aucune modif `index.ts`** (hal-mcp lit déjà `workspace_members`). Review 5 agents : **0 CRITICAL/0 HIGH** ; M4/L1/L2 fixés inline ; **M3 replié avant merge par choix Renaud** (`RAISE WARNING` non-fatal quand aucun workspace ne matche — aligne fail-loudly sans casser « never fail the signup »). M1/M2/M5/L3 → follow-ups #61-#64. CI verte (Deno + pytest). **Appliqué en prod `zgkvbjqlvebttbnkklpo` via `apply_migration` (2026-07-19)** — 1er apply a échoué sur `42P17` (`WHEN` d'un trigger INSERT-OR-UPDATE ne peut pas référencer `OLD`) → **fix-forward** : garde « company_id changé » déplacée dans le corps de la fonction (`TG_OP='UPDATE'`), `WHEN` réduit à company_id non-null. Vérifié en prod : structure (SECURITY DEFINER, search_path), backfill Yani (`ic-ingenieurs-conseils`), + **test comportemental rollback** (fire+insert sur nouveau company_id, no-op sur company_id inchangé). Fichier de migration corrigé pour matcher le SQL appliqué.
- [x] **Plugin packaging fix + CLI install → headless test PASSED — 2026-07-11** — le test headless post-2.4 était bloqué par un bug d'empaquetage **pré-existant** (orthogonal à 2.4). Dans `marketplace.json`, chaque plugin avait `source: ./plugins/<name>` **+** un `skills[]` en chemin repo-root → préfixe `source` **doublé** à la résolution (`source + skill_path`) → `path not found` → **échec de chargement CLI** (renaud ×4 + bluegreen `hal`). Fix : **suppression des `skills[]`** → auto-découverte depuis `<source>/skills/` (pattern Cowork officiel Anthropic `knowledge-work-plugins`) — PR renaud [#59](https://github.com/BluegReeno/renaud-marketplace/pull/59) + bluegreen [#38](https://github.com/BluegReeno/bluegreen-marketplace/pull/38), mergées (CI verte, map improve inchangée). renaud-marketplace ajoutée comme **marketplace CLI** (`claude plugin marketplace add`) ; 5 plugins `✔ enabled` (briefing 0.9.0, hal 0.10.1, jobsearch, improve, myspy) ; orphelin `edifice-mission-report` retiré ; copie obsolète `~/.claude/skills/morning-briefing` (Feb/Notion) archivée dans `~/.claude/skills-archived/`. **Cowork non impacté** (panneaux plugins isolés). **Commande headless correcte = `/briefing:morning-briefing --headless`** (skills namespacés `/<plugin>:<skill>` en `-p`). **Test PASSED** : hal joignable → 3 daily-logs écrits, CV skip visible, plan `[proposé — non validé]` non écrit hal, `jobsearch:DOWN` visible. ⚠️ **Phase 3** : en headless `jobsearch-vault` DOWN (invocation imbriquée + vault non monté) → pipeline reconstitué depuis daily-log, pas live. Mémo : `reference_plugin_packaging`.
Expand Down
7 changes: 7 additions & 0 deletions docs/cross-repo-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,13 @@ Keep entries ≤ 10 lines. Link outward; don't restate.
- **Verified on prod** in a rolled-back transaction: fresh 2-step invite (INSERT empty app_metadata → UPDATE with company_id + allowed_apps) auto-populates `organization_id` after step 2; `lmgarcia` stays NULL; 0 residue after rollback.
- **Follow-up**: none required. Optional PWA invite UI stays deferred (brief §8). CI net (hal #65, migration smoke-test) applies here too.

## 2026-07-22 — hal: guard ::uuid cast in provisioning trigger against malformed company_id (#62)

- **Repo**: hal (DB only — `workspace_members` is hal-owned; no code/schema touched elsewhere)
- **What shipped**: New migration `supabase/migrations/20260722195616_guard_company_id_uuid_cast.sql` — trigger-only fix-forward for the already-live `trg_provision_workspace_membership` (from `20260719000000`). The function's `DECLARE` casts `raw_app_meta_data->>'company_id'` to `uuid` unconditionally; the old `WHEN` only checked non-emptiness, so a non-UUID value raised `22P02` inside `DECLARE` (before the body's "never fail the signup" machinery) and aborted the triggering `auth.users` write. Fix: the `WHEN` clause now also regex-validates the canonical UUID shape (`~* '^[0-9a-f]{8}-...-[0-9a-f]{12}$'`, case-insensitive) — a malformed value fails `WHEN`, the function never runs, the `auth.users` write commits, provisioning is silently skipped for that row. Function body **unchanged** (no `CREATE OR REPLACE FUNCTION`); `WHEN` references only `NEW`, so no `42P17` regression. **Applied to prod `zgkvbjqlvebttbnkklpo`** via `apply_migration`.
- **Verified on prod** in a rolled-back transaction (Yani `aa7b0527-…`): malformed `company_id="not-a-uuid"` UPDATE now **commits** (no `22P02`) and creates **no** `workspace_members` row; a valid UUID matching `halcrm_workspaces` (blue-green `26a1de94-…`) still provisions (member count 6→7); 0 residue after rollback. The malformed test was first run with `allowed_apps` stripped to isolate this trigger (see next bullet).
- **⚠️ Impact on edifice — the companion trigger has the SAME unguarded cast, still unfixed**: `trg_provision_edifice_org` (edifice PR/issue #61, `provision_edifice_org()`) also lives on `auth.users` and casts `company_id::uuid` unconditionally in its function body, gated by `WHEN (… company_id non-null AND jsonb_exists(allowed_apps,'edifice'))`. For any user with `allowed_apps: edifice` (e.g. Yani, Steeve), a malformed `company_id` **still raises `22P02` and still aborts the invite** — the auth.users-level invite is therefore **not fully protected** until edifice ships the mirror guard. Confirmed empirically: the first (non-isolated) rollback test above hit `22P02` from `provision_edifice_org`, not from the hal trigger. **Action: file an edifice issue mirroring #62** (add the UUID-format regex to `trg_provision_edifice_org`'s `WHEN`) — edifice-owned (Steeve), same fix shape as this one.
- **Follow-up**: (a) edifice mirror-guard issue (above) — required for full invite protection. (b) hal follow-ups from PR #60 review still open: #61 (M1 UNIQUE company_id), #63 (M5 checklist), #64 (L3 CLAUDE.md doc). (c) PR #66 review flagged that the malformed-`company_id` skip is fully silent (a `WHEN` clause cannot `RAISE`, unlike the sibling "no matching workspace" case) — filed as hal issue [#67](https://github.com/BluegReeno/hal/issues/67) (periodic ops check), not fixed inline (would require `CREATE OR REPLACE FUNCTION`, out of this PR's scope).
## 2026-07-22 — hal: UNIQUE (company_id) on halcrm_workspaces (issue #61)

- **Repo**: hal (DB only — `halcrm_workspaces` is hal-owned; no `edifice_*` table touched)
Expand Down
30 changes: 30 additions & 0 deletions supabase/migrations/20260722195616_guard_company_id_uuid_cast.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Guard the ::uuid cast in provision_workspace_membership against a malformed company_id.
-- hal-owned. The trigger from 20260719000000 fires whenever raw_app_meta_data.company_id
-- is non-empty text, then the function's DECLARE casts it to uuid unconditionally. A
-- non-UUID value raises 22P02 inside DECLARE — before the function body's "never fail the
-- signup" machinery can intercept it — which aborts the triggering auth.users write and
-- breaks the invite. Adding a UUID-format check to the trigger's WHEN clause makes a
-- malformed value fail WHEN, so the function (and its unguarded cast) never runs: the
-- auth.users write commits, provisioning is skipped for that row.
--
-- Trigger-only diff — the function body is unchanged, so no CREATE OR REPLACE FUNCTION.
-- WHEN references only NEW (never OLD), so this cannot resurrect the 42P17 class of error
-- already fixed once in the original file (the "company_id actually changed" guard stays
-- in the function body under TG_OP = 'UPDATE').
--
-- The 20260719000000 trigger is already live in prod, so per this repo's fix-forward rule
-- this ships as a NEW migration, applied to zgkvbjqlvebttbnkklpo via apply_migration
-- (Supabase MCP) or the dashboard SQL editor — NEVER `supabase db push` from this repo.
DROP TRIGGER IF EXISTS trg_provision_workspace_membership ON auth.users;
CREATE TRIGGER trg_provision_workspace_membership
AFTER INSERT OR UPDATE OF raw_app_meta_data ON auth.users
FOR EACH ROW
WHEN (
NULLIF(NEW.raw_app_meta_data->>'company_id','') IS NOT NULL
-- Canonical dashed form only (auth-gateway is the sole writer and always emits
-- this form) — Postgres's own ::uuid parser also accepts undashed/braced/urn:
-- forms that this regex intentionally excludes.
AND NEW.raw_app_meta_data->>'company_id' ~*
'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
)
EXECUTE FUNCTION public.provision_workspace_membership();
Loading