fix: tier featured dans feature-gate + TIER_RANK + scripts manquants
This commit is contained in:
@@ -121,7 +121,7 @@ FEATURE_TIER: dict[str, str] = {
|
||||
'bsi': 'free',
|
||||
}
|
||||
|
||||
TIER_RANK = {'free': 0, 'pro': 1, 'owner': 2, 'full': 2} # 'full' = alias owner
|
||||
TIER_RANK = {'free': 0, 'featured': 1, 'pro': 2, 'owner': 3, 'full': 3} # chaîne: free → featured → pro → full
|
||||
|
||||
# ── Tier cache ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -332,14 +332,15 @@ def _load_catalog(agents_dir: Path) -> dict:
|
||||
|
||||
|
||||
# Tier access hierarchy: owner sees all, pro sees pro+free, free sees only free
|
||||
_CATALOG_TIER_RANK: dict[str, int] = {'free': 0, 'pro': 1, 'owner': 2}
|
||||
_CATALOG_TIER_RANK: dict[str, int] = {'free': 0, 'featured': 1, 'pro': 2, 'owner': 3}
|
||||
|
||||
# Map token tier → max catalog tier accessible
|
||||
_TOKEN_TIER_TO_CATALOG: dict[str, str] = {
|
||||
'free': 'free',
|
||||
'mcp': 'pro',
|
||||
'pro': 'pro',
|
||||
'owner': 'owner',
|
||||
'free': 'free',
|
||||
'featured': 'featured',
|
||||
'mcp': 'pro',
|
||||
'pro': 'pro',
|
||||
'owner': 'owner',
|
||||
}
|
||||
|
||||
|
||||
|
||||
231
scripts/bsi-claim.sh
Executable file
231
scripts/bsi-claim.sh
Executable file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env bash
|
||||
# bsi-claim.sh — Open/close claims dans brain.db (source unique — ADR-042)
|
||||
#
|
||||
# Usage :
|
||||
# bsi-claim.sh open <sess_id> [--scope X] [--type X] [--zone X] [--mode X] [--story "X"]
|
||||
# bsi-claim.sh close <sess_id> [--result X]
|
||||
# bsi-claim.sh close-stale → ferme tous les claims open > TTL (4h par défaut)
|
||||
# bsi-claim.sh exists <sess_id> → exit 0 si open, exit 1 sinon
|
||||
# bsi-claim.sh init → crée brain.db + table claims si absent
|
||||
#
|
||||
# Garantie tier free : python3 + sqlite3 stdlib — zéro dépendance externe.
|
||||
# Auto-init : si brain.db ou table claims absente → créée automatiquement.
|
||||
#
|
||||
# Exit codes :
|
||||
# 0 = succès
|
||||
# 1 = argument manquant / erreur usage
|
||||
# 2 = erreur Python / DB
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BRAIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DB_PATH="$BRAIN_ROOT/brain.db"
|
||||
CMD="${1:-help}"
|
||||
shift || true
|
||||
|
||||
python3 - "$DB_PATH" "$CMD" "$@" <<'PYEOF'
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
db_path = sys.argv[1]
|
||||
cmd = sys.argv[2] if len(sys.argv) > 2 else "help"
|
||||
args = sys.argv[3:]
|
||||
|
||||
CLAIMS_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS claims (
|
||||
sess_id TEXT PRIMARY KEY,
|
||||
type TEXT,
|
||||
scope TEXT,
|
||||
status TEXT DEFAULT 'open',
|
||||
opened_at TEXT,
|
||||
closed_at TEXT,
|
||||
handoff_level TEXT,
|
||||
story_angle TEXT,
|
||||
health_score REAL,
|
||||
context_at_close REAL,
|
||||
cold_start_kpi_pass INTEGER,
|
||||
ttl_hours REAL DEFAULT 4.0,
|
||||
expires_at TEXT,
|
||||
instance TEXT,
|
||||
parent_sess TEXT,
|
||||
satellite_type TEXT,
|
||||
satellite_level TEXT,
|
||||
theme_branch TEXT,
|
||||
zone TEXT,
|
||||
mode TEXT,
|
||||
workflow TEXT,
|
||||
workflow_step INTEGER,
|
||||
result_status TEXT,
|
||||
result_json TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
def get_db():
|
||||
"""Connect and ensure table exists (auto-init for fresh forks)."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute(CLAIMS_SCHEMA)
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
def parse_opts(args):
|
||||
"""Parse --key value pairs from args."""
|
||||
opts = {}
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i].startswith("--") and i + 1 < len(args):
|
||||
opts[args[i][2:]] = args[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
return opts
|
||||
|
||||
def cmd_open():
|
||||
if not args:
|
||||
print("❌ Usage: bsi-claim.sh open <sess_id> [--scope X] [--type X] ...", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sess_id = args[0]
|
||||
opts = parse_opts(args[1:])
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M")
|
||||
|
||||
conn = get_db()
|
||||
|
||||
# Vérifier si déjà open
|
||||
existing = conn.execute(
|
||||
"SELECT status FROM claims WHERE sess_id = ?", (sess_id,)
|
||||
).fetchone()
|
||||
if existing and existing["status"] == "open":
|
||||
print(f"⚠️ Claim déjà ouvert : {sess_id}")
|
||||
conn.close()
|
||||
sys.exit(0)
|
||||
|
||||
new_scope = opts.get("scope", "brain/")
|
||||
|
||||
# Scope overlap detection — BSI mutex
|
||||
open_claims = conn.execute(
|
||||
"SELECT sess_id, scope, zone FROM claims WHERE status = 'open'"
|
||||
).fetchall()
|
||||
|
||||
for oc in open_claims:
|
||||
oc_scope = oc["scope"] or ""
|
||||
# Overlap = un scope est préfixe de l'autre, ou identique
|
||||
if (new_scope.startswith(oc_scope) or oc_scope.startswith(new_scope)
|
||||
or new_scope == oc_scope):
|
||||
oc_zone = oc["zone"] or "project"
|
||||
|
||||
# Zone kernel = hard block
|
||||
if oc_zone == "kernel" or opts.get("zone") == "kernel":
|
||||
print(f"🔴 SCOPE CONFLICT — zone kernel verrouillée")
|
||||
print(f" Existant : {oc['sess_id']} → scope: {oc_scope} (zone: {oc_zone})")
|
||||
print(f" Demandé : {sess_id} → scope: {new_scope}")
|
||||
print(f" → Fermer le claim existant d'abord : bsi-claim.sh close {oc['sess_id']}")
|
||||
conn.close()
|
||||
sys.exit(1)
|
||||
|
||||
# Zone project = soft warning (parallélisme autorisé avec avertissement)
|
||||
print(f"⚠️ SCOPE OVERLAP détecté")
|
||||
print(f" Existant : {oc['sess_id']} → scope: {oc_scope}")
|
||||
print(f" Demandé : {sess_id} → scope: {new_scope}")
|
||||
print(f" → Parallélisme autorisé — attention aux conflits d'écriture")
|
||||
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO claims
|
||||
(sess_id, type, scope, status, opened_at, zone, mode, story_angle,
|
||||
handoff_level, instance, ttl_hours, expires_at)
|
||||
VALUES (?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 4.0, datetime(?, '+4 hours'))
|
||||
""", (
|
||||
sess_id,
|
||||
opts.get("type", "navigate"),
|
||||
new_scope,
|
||||
now,
|
||||
opts.get("zone", "project"),
|
||||
opts.get("mode"),
|
||||
opts.get("story"),
|
||||
opts.get("handoff", "0"),
|
||||
opts.get("instance"),
|
||||
now,
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"✅ Claim ouvert : {sess_id}")
|
||||
|
||||
def cmd_close():
|
||||
if not args:
|
||||
print("❌ Usage: bsi-claim.sh close <sess_id> [--result X]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sess_id = args[0]
|
||||
opts = parse_opts(args[1:])
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M")
|
||||
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"UPDATE claims SET status = 'closed', closed_at = ?, result_status = ? WHERE sess_id = ? AND status = 'open'",
|
||||
(now, opts.get("result", "success"), sess_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
if cur.rowcount == 0:
|
||||
print(f"⚠️ Claim non trouvé ou déjà fermé : {sess_id}")
|
||||
else:
|
||||
print(f"✅ Claim fermé : {sess_id}")
|
||||
conn.close()
|
||||
|
||||
def cmd_close_stale():
|
||||
conn = get_db()
|
||||
cur = conn.execute("""
|
||||
UPDATE claims
|
||||
SET status = 'closed',
|
||||
closed_at = datetime('now'),
|
||||
result_status = 'stale-auto-closed'
|
||||
WHERE status = 'open'
|
||||
AND julianday('now') > julianday(opened_at, '+' || COALESCE(ttl_hours, 4) || ' hours')
|
||||
""")
|
||||
conn.commit()
|
||||
n = cur.rowcount
|
||||
if n > 0:
|
||||
print(f"✅ {n} claim(s) stale fermé(s)")
|
||||
else:
|
||||
print("ℹ️ Aucun claim stale")
|
||||
conn.close()
|
||||
|
||||
def cmd_exists():
|
||||
if not args:
|
||||
print("❌ Usage: bsi-claim.sh exists <sess_id>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT status FROM claims WHERE sess_id = ? AND status = 'open'", (args[0],)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
sys.exit(0 if row else 1)
|
||||
|
||||
def cmd_init():
|
||||
conn = get_db()
|
||||
n = conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0]
|
||||
conn.close()
|
||||
print(f"✅ brain.db prêt — table claims ({n} entrées)")
|
||||
|
||||
def cmd_help():
|
||||
print("Usage: bsi-claim.sh <open|close|close-stale|exists|init>")
|
||||
print(" open <sess_id> [--scope X] [--type X] [--zone X] [--mode X] [--story 'X']")
|
||||
print(" close <sess_id> [--result X]")
|
||||
print(" close-stale — ferme les claims open > TTL")
|
||||
print(" exists <sess_id> — exit 0 si open, exit 1 sinon")
|
||||
print(" init — crée brain.db + table si absent")
|
||||
|
||||
commands = {
|
||||
"open": cmd_open,
|
||||
"close": cmd_close,
|
||||
"close-stale": cmd_close_stale,
|
||||
"exists": cmd_exists,
|
||||
"init": cmd_init,
|
||||
"help": cmd_help,
|
||||
}
|
||||
|
||||
fn = commands.get(cmd, cmd_help)
|
||||
fn()
|
||||
PYEOF
|
||||
99
scripts/feature-gate-check.sh
Executable file
99
scripts/feature-gate-check.sh
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
# feature-gate-check.sh — Vérifie si une feature ou un tier est activé
|
||||
# Returns 0 (enabled) / 1 (disabled)
|
||||
#
|
||||
# Usage :
|
||||
# bash scripts/feature-gate-check.sh bact.enrichment
|
||||
# bash scripts/feature-gate-check.sh pro # tier level check
|
||||
# if bash scripts/feature-gate-check.sh diagram.actions; then ...
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
BRAIN_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
|
||||
COMPOSE_FILE="$BRAIN_ROOT/brain-compose.local.yml"
|
||||
|
||||
# --- Lire le tier actif depuis brain-compose.local.yml ---
|
||||
_get_tier() {
|
||||
[ -f "$COMPOSE_FILE" ] || { echo "free"; return; }
|
||||
local tier="free"
|
||||
if command -v python3 &>/dev/null && python3 -c "import yaml" &>/dev/null 2>&1; then
|
||||
tier=$(BRAIN_COMPOSE="$COMPOSE_FILE" python3 - <<'PYEOF' 2>/dev/null
|
||||
import yaml, os, sys
|
||||
path = os.environ.get('BRAIN_COMPOSE', '')
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
instances = data.get('instances', {})
|
||||
for name, inst in instances.items():
|
||||
if inst.get('active'):
|
||||
print(inst.get('feature_set', {}).get('tier', 'free'))
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
pass
|
||||
print('free')
|
||||
PYEOF
|
||||
)
|
||||
else
|
||||
# Fallback grep — fonctionne sur brain-compose.local.yml standard
|
||||
tier=$(grep "^\s*tier:" "$COMPOSE_FILE" | head -1 | awk '{print $NF}' | tr -d "'\"")
|
||||
fi
|
||||
echo "${tier:-free}"
|
||||
}
|
||||
|
||||
# --- Niveau numérique du tier ---
|
||||
_tier_level() {
|
||||
case "$1" in
|
||||
free) echo 0 ;;
|
||||
featured) echo 1 ;;
|
||||
pro) echo 2 ;;
|
||||
full) echo 3 ;;
|
||||
*) echo 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- Tier minimum requis par feature ---
|
||||
_feature_min_tier() {
|
||||
case "$1" in
|
||||
# tier: free — toujours enabled
|
||||
kernel.boot|kernel.agents|workflow.manual|diagram.readonly)
|
||||
echo "free" ;;
|
||||
# tier: featured — coaching + distillation RAG
|
||||
coach.full|distillation.rag|progression.tracking)
|
||||
echo "featured" ;;
|
||||
# tier: pro — outils metier
|
||||
bact.enrichment|workflow.orchestrated|diagram.interactive|supervisor.project)
|
||||
echo "pro" ;;
|
||||
# tier: full — kernel + supervision
|
||||
bact.rag|diagram.actions|kernel.write)
|
||||
echo "full" ;;
|
||||
# feature inconnue → false (défaut sécurisé)
|
||||
*)
|
||||
echo "unknown" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
FEATURE="${1:-}"
|
||||
if [ -z "$FEATURE" ]; then
|
||||
echo "Usage: feature-gate-check.sh <feature|tier>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
CURRENT_TIER=$(_get_tier)
|
||||
CURRENT_LEVEL=$(_tier_level "$CURRENT_TIER")
|
||||
|
||||
# Cas 1 : argument est un tier name (free/featured/pro/full)
|
||||
case "$FEATURE" in
|
||||
free|featured|pro|full)
|
||||
REQUIRED_LEVEL=$(_tier_level "$FEATURE")
|
||||
[ "$CURRENT_LEVEL" -ge "$REQUIRED_LEVEL" ] && exit 0 || exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Cas 2 : argument est un feature name
|
||||
MIN_TIER=$(_feature_min_tier "$FEATURE")
|
||||
if [ "$MIN_TIER" = "unknown" ]; then
|
||||
exit 1
|
||||
fi
|
||||
REQUIRED_LEVEL=$(_tier_level "$MIN_TIER")
|
||||
[ "$CURRENT_LEVEL" -ge "$REQUIRED_LEVEL" ] && exit 0 || exit 1
|
||||
Reference in New Issue
Block a user