sync: 172 file da Baida98/AI@789d6ce1 (2026-08-22 17:46 UTC) [deploy-all]

#55
by Baida07 - opened
api/admin_state.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stato operativo amministrativo protetto da JWT Supabase admin."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, Depends, Query
8
+
9
+ from .auth_guard import require_admin_user
10
+ from .private_state import _MAX_TASK_PAGE, _as_epoch_ms, _call, _json_object
11
+
12
+ router = APIRouter(
13
+ prefix="/api/admin/state",
14
+ tags=["admin"],
15
+ dependencies=[Depends(require_admin_user)],
16
+ )
17
+
18
+
19
+ @router.get("/sessions")
20
+ async def admin_sessions(
21
+ max_age_ms: int = Query(default=300_000, ge=10_000, le=3_600_000),
22
+ limit: int = Query(default=100, ge=1, le=200),
23
+ ) -> dict[str, object]:
24
+ cutoff = (datetime.now(timezone.utc) - timedelta(milliseconds=max_age_ms)).isoformat()
25
+
26
+ def operation(client: Any):
27
+ return client.table("agent_tasks").select("task_id,context,updated_at").eq("status", "__session__").gte("updated_at", cutoff).order("updated_at", desc=True).limit(limit).execute()
28
+
29
+ result = await _call(operation)
30
+ sessions = []
31
+ for row in result.data or []:
32
+ context = _json_object(row.get("context"))
33
+ session_id = str(context.get("sessionId") or row.get("task_id") or "").strip()
34
+ if not session_id:
35
+ continue
36
+ claimed = context.get("claimedFiles")
37
+ sessions.append({
38
+ "session_id": session_id,
39
+ "session_name": str(context.get("sessionName") or session_id)[:160],
40
+ "sprint": str(context["sprint"])[:120] if context.get("sprint") else None,
41
+ "claimed_files": [str(item)[:300] for item in claimed[:100]] if isinstance(claimed, list) else [],
42
+ "last_heartbeat": _as_epoch_ms(context.get("lastHeartbeat")) or _as_epoch_ms(row.get("updated_at")),
43
+ "current_task": str(context["currentTask"])[:500] if context.get("currentTask") else None,
44
+ })
45
+ return {"sessions": sessions}
46
+
47
+
48
+ @router.get("/tasks")
49
+ async def admin_tasks(
50
+ limit: int = Query(default=20, ge=1, le=_MAX_TASK_PAGE),
51
+ offset: int = Query(default=0, ge=0, le=10_000),
52
+ status: str | None = Query(default=None, max_length=64),
53
+ ) -> dict[str, object]:
54
+ normalized_status = status.strip().upper() if status else ""
55
+
56
+ def operation(client: Any):
57
+ query = client.table("agent_tasks").select("task_id,goal,status,updated_at").neq("status", "__session__").neq("status", "__config__")
58
+ if normalized_status:
59
+ query = query.eq("status", normalized_status)
60
+ page = query.order("updated_at", desc=True).range(offset, offset + limit - 1).execute()
61
+ all_statuses = client.table("agent_tasks").select("status").neq("status", "__session__").neq("status", "__config__").limit(2_000).execute()
62
+ return page, all_statuses
63
+
64
+ page, all_statuses = await _call(operation)
65
+ counts: dict[str, int] = {}
66
+ for row in all_statuses.data or []:
67
+ key = str(row.get("status") or "UNKNOWN").upper()
68
+ counts[key] = counts.get(key, 0) + 1
69
+ tasks = [{
70
+ "task_id": str(row.get("task_id") or ""),
71
+ "goal": str(row.get("goal") or "")[:1_000],
72
+ "status": str(row.get("status") or "UNKNOWN"),
73
+ "updated_at": _as_epoch_ms(row.get("updated_at")),
74
+ } for row in page.data or []]
75
+ return {"tasks": tasks, "counts": counts, "offset": offset, "limit": limit}
api/auth_guard.py CHANGED
@@ -33,7 +33,7 @@ from __future__ import annotations
33
  import logging
34
  import os
35
  from enum import IntEnum
36
- from typing import Optional
37
 
38
  from fastapi import Depends, Header, HTTPException, Request
39
 
@@ -187,6 +187,66 @@ def _check_rate_limit(
187
  return _inmem_rate_check(key, limit, int(_RATE_WINDOW_S))
188
 
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  class AuthRole(IntEnum):
191
  """Gerarchia ruoli: USER < MACHINE < OPERATOR < ADMIN."""
192
  USER = 0
 
33
  import logging
34
  import os
35
  from enum import IntEnum
36
+ from typing import Optional, Any
37
 
38
  from fastapi import Depends, Header, HTTPException, Request
39
 
 
187
  return _inmem_rate_check(key, limit, int(_RATE_WINDOW_S))
188
 
189
 
190
+ async def require_supabase_user(request: Request) -> dict[str, Any]:
191
+ """Valida il Bearer JWT tramite Supabase Auth e restituisce il profilo minimo.
192
+
193
+ La chiave Supabase resta server-side; il JWT arriva esclusivamente nell'header
194
+ Authorization del chiamante e non viene scritto nei log.
195
+ """
196
+ import httpx
197
+
198
+ authorization = request.headers.get("Authorization", "")
199
+ if not authorization.lower().startswith("bearer "):
200
+ raise HTTPException(status_code=401, detail="Bearer token richiesto")
201
+ jwt = authorization[7:].strip()
202
+ if not jwt:
203
+ raise HTTPException(status_code=401, detail="Bearer token non valido")
204
+
205
+ supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/")
206
+ api_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("SUPABASE_KEY", "")
207
+ if not supabase_url or not api_key:
208
+ raise HTTPException(status_code=503, detail="Autenticazione Supabase non configurata")
209
+
210
+ try:
211
+ async with httpx.AsyncClient(timeout=5) as client:
212
+ response = await client.get(
213
+ f"{supabase_url}/auth/v1/user",
214
+ headers={
215
+ "apikey": api_key,
216
+ "Authorization": f"Bearer {jwt}",
217
+ "Accept": "application/json",
218
+ },
219
+ )
220
+ except httpx.HTTPError as exc:
221
+ logger.warning("supabase user validation unavailable: %s", type(exc).__name__)
222
+ raise HTTPException(status_code=503, detail="Autenticazione temporaneamente non disponibile") from exc
223
+
224
+ if response.status_code != 200:
225
+ raise HTTPException(status_code=401, detail="Sessione Supabase non valida o scaduta")
226
+ try:
227
+ user = response.json()
228
+ except ValueError as exc:
229
+ raise HTTPException(status_code=401, detail="Risposta autenticazione non valida") from exc
230
+ if not isinstance(user, dict) or not user.get("id"):
231
+ raise HTTPException(status_code=401, detail="Utente Supabase non valido")
232
+ return user
233
+
234
+
235
+ async def require_admin_user(request: Request) -> dict[str, Any]:
236
+ """Richiede un JWT Supabase con app_metadata.role=admin.
237
+
238
+ app_metadata Γ¨ server-controlled; user_metadata non viene mai considerato
239
+ per autorizzare l’area amministrativa.
240
+ """
241
+ user = await require_supabase_user(request)
242
+ app_metadata = user.get("app_metadata") or {}
243
+ roles = app_metadata.get("roles") or []
244
+ is_admin = app_metadata.get("role") == "admin" or "admin" in roles
245
+ if not is_admin:
246
+ raise HTTPException(status_code=403, detail="Membership amministrativa richiesta")
247
+ return user
248
+
249
+
250
  class AuthRole(IntEnum):
251
  """Gerarchia ruoli: USER < MACHINE < OPERATOR < ADMIN."""
252
  USER = 0
api/me_tasks.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task personali del prodotto pubblico.
2
+
3
+ Tutte le query applicano owner_id derivato dal JWT Supabase verificato. Il client
4
+ non puΓ² scegliere o sostituire il proprietario nel body o nella query.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import logging
10
+ from typing import Any
11
+ from uuid import UUID
12
+
13
+ from fastapi import APIRouter, Depends, HTTPException, Query
14
+ from pydantic import BaseModel, Field
15
+
16
+ from .auth_guard import require_supabase_user
17
+ from .state import sb
18
+
19
+ _logger = logging.getLogger("agente_ai.api.me_tasks")
20
+ router = APIRouter(prefix="/api/me/tasks", tags=["me"])
21
+
22
+
23
+ class TaskCreate(BaseModel):
24
+ goal: str = Field(min_length=1, max_length=10_000)
25
+
26
+
27
+ class TaskUpdate(BaseModel):
28
+ status: str = Field(pattern="^(queued|in_progress|done|failed|cancelled)$")
29
+
30
+
31
+ _ALLOWED = "id,goal,status,created_at,updated_at"
32
+
33
+
34
+ def _owner(user: dict[str, Any]) -> str:
35
+ return str(user["id"])
36
+
37
+
38
+ def _client():
39
+ client = sb()
40
+ if client is None:
41
+ raise HTTPException(status_code=503, detail="Database non configurato")
42
+ return client
43
+
44
+
45
+ @router.get("")
46
+ async def list_my_tasks(
47
+ user: dict[str, Any] = Depends(require_supabase_user),
48
+ limit: int = Query(50, ge=1, le=100),
49
+ offset: int = Query(0, ge=0),
50
+ ) -> dict[str, Any]:
51
+ client = _client()
52
+ owner_id = _owner(user)
53
+
54
+ def operation():
55
+ return client.table("user_agent_tasks").select(_ALLOWED).eq("owner_id", owner_id).order("updated_at", desc=True).range(offset, offset + limit - 1).execute()
56
+
57
+ try:
58
+ result = await asyncio.to_thread(operation)
59
+ return {"tasks": result.data or [], "offset": offset, "limit": limit}
60
+ except Exception as exc:
61
+ _logger.warning("list own tasks failed: %s", type(exc).__name__)
62
+ raise HTTPException(status_code=503, detail="Task personali temporaneamente non disponibili") from exc
63
+
64
+
65
+ @router.post("", status_code=201)
66
+ async def create_my_task(
67
+ body: TaskCreate,
68
+ user: dict[str, Any] = Depends(require_supabase_user),
69
+ ) -> dict[str, Any]:
70
+ client = _client()
71
+ owner_id = _owner(user)
72
+
73
+ def operation():
74
+ return client.table("user_agent_tasks").insert({"owner_id": owner_id, "goal": body.goal.strip(), "status": "queued"}).select(_ALLOWED).single().execute()
75
+
76
+ try:
77
+ result = await asyncio.to_thread(operation)
78
+ if not result.data:
79
+ raise HTTPException(status_code=502, detail="Task personale non creato")
80
+ return result.data
81
+ except HTTPException:
82
+ raise
83
+ except Exception as exc:
84
+ _logger.warning("create own task failed: %s", type(exc).__name__)
85
+ raise HTTPException(status_code=503, detail="Task personale temporaneamente non disponibile") from exc
86
+
87
+
88
+ @router.patch("/{task_id}")
89
+ async def update_my_task(
90
+ task_id: UUID,
91
+ body: TaskUpdate,
92
+ user: dict[str, Any] = Depends(require_supabase_user),
93
+ ) -> dict[str, Any]:
94
+ client = _client()
95
+ owner_id = _owner(user)
96
+
97
+ def operation():
98
+ return client.table("user_agent_tasks").update({"status": body.status}).eq("id", str(task_id)).eq("owner_id", owner_id).select(_ALLOWED).maybe_single().execute()
99
+
100
+ try:
101
+ result = await asyncio.to_thread(operation)
102
+ if not result.data:
103
+ raise HTTPException(status_code=404, detail="Task personale non trovato")
104
+ return result.data
105
+ except HTTPException:
106
+ raise
107
+ except Exception as exc:
108
+ _logger.warning("update own task failed: %s", type(exc).__name__)
109
+ raise HTTPException(status_code=503, detail="Task personale temporaneamente non disponibile") from exc
110
+
111
+
112
+ @router.post("/{task_id}/cancel")
113
+ async def cancel_my_task(
114
+ task_id: UUID,
115
+ user: dict[str, Any] = Depends(require_supabase_user),
116
+ ) -> dict[str, Any]:
117
+ return await update_my_task(task_id, TaskUpdate(status="cancelled"), user)
api/public_status.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DTO pubblico e sanificato dello stato del servizio.
2
+
3
+ Questa route non legge agent_tasks, sessioni operative o log. La tabella
4
+ public_dashboard_snapshot viene aggiornata dal backend con service_role e letta
5
+ qui tramite una whitelist di campi.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import Any
12
+
13
+ from fastapi import APIRouter, HTTPException
14
+
15
+ from .state import sb
16
+
17
+ _logger = logging.getLogger("agente_ai.api.public_status")
18
+ router = APIRouter(prefix="/api/public", tags=["public"])
19
+
20
+ _PUBLIC_FIELDS = (
21
+ "singleton,service_status,active_sessions,queued_tasks,in_progress_tasks,"
22
+ "app_version,updated_at"
23
+ )
24
+
25
+
26
+ @router.get("/status")
27
+ async def public_status() -> dict[str, Any]:
28
+ """Restituisce esclusivamente lo snapshot deliberatamente pubblico."""
29
+ client = sb()
30
+ if client is None:
31
+ raise HTTPException(status_code=503, detail="Public status non configurato")
32
+
33
+ def operation():
34
+ return client.table("public_dashboard_snapshot").select(_PUBLIC_FIELDS).eq("singleton", True).limit(1).execute()
35
+
36
+ try:
37
+ result = await asyncio.to_thread(operation)
38
+ except Exception as exc:
39
+ _logger.warning("public status snapshot unavailable: %s", type(exc).__name__)
40
+ raise HTTPException(status_code=503, detail="Public status temporaneamente non disponibile") from exc
41
+
42
+ row = (result.data or [None])[0]
43
+ if not row:
44
+ raise HTTPException(status_code=503, detail="Public status snapshot non inizializzato")
45
+
46
+ return {
47
+ "service_status": str(row.get("service_status") or "unknown"),
48
+ "active_sessions": int(row.get("active_sessions") or 0),
49
+ "queued_tasks": int(row.get("queued_tasks") or 0),
50
+ "in_progress_tasks": int(row.get("in_progress_tasks") or 0),
51
+ "app_version": row.get("app_version"),
52
+ "updated_at": row.get("updated_at"),
53
+ }
benchmark-extended.mjs CHANGED
@@ -244,10 +244,10 @@ FSH.orchestration =
244
  "ESECUZIONE: esegui ogni step nell'ordine con i tool dichiarati.";
245
 
246
  FSH.bug_fix =
247
- "RISPOSTA IN TRE SEZIONI:\n" +
248
- "1. **Root cause**: [1 riga β€” causa esatta del bug]\n" +
249
- "2. **Fix** (TypeScript compilabile)\n" +
250
- "3. **Test**: [unit test che verifica il fix]";
251
 
252
  FSH.refactor =
253
  "TECNICA OBBLIGATORIA: guard clauses + early return.\n" +
@@ -2443,6 +2443,10 @@ async function runOneSeed(seed,opts={}){
2443
  const reportPath=`/tmp/agente-ai/benchmark-v5-${seed}.json`;
2444
  const report={
2445
  timestamp:new Date().toISOString(),version:"extended-v5",
 
 
 
 
2446
  seed,replayCli:`node benchmark-extended.mjs --seed ${seed}`,
2447
  spaceVersion:spaceV,
2448
  specCoverage:{A_ragionamento:"reasoning",B_orchestrazione:"orchestration",
@@ -2589,9 +2593,13 @@ else if(MULTI>1){
2589
  : null,
2590
  avgDevin: _runResult.avgDevin,
2591
  avgManus: _runResult.avgManus,
2592
- verdict: _runResult.avgScore >= _runResult.avgReplit ? "PARI_REPLIT" : "SOTTO_REPLIT",
2593
  };
2594
- saveBenchmarkReport("benchmark-extended-single", { summary, tasks: _runResult });
 
 
 
 
2595
  } catch (e) { console.log(`${R}⚠️ Report engine error: ${e.message}${NC}`); }
2596
 
2597
  if(F_IMPROVE){ await runImprovementCycle(_runResult,_runResult.selectedTasks); }
 
244
  "ESECUZIONE: esegui ogni step nell'ordine con i tool dichiarati.";
245
 
246
  FSH.bug_fix =
247
+ "OUTPUT VINCOLATO: restituisci esclusivamente un singolo blocco ```typescript ... ``` completo e compilabile.\n" +
248
+ "Non aggiungere Root cause, Test, spiegazioni o testo fuori dal blocco.\n" +
249
+ "Mantieni la struttura e le firme pubbliche; per effetti React includi guardia di smontaggio e cleanup/abort nel return di useEffect.\n" +
250
+ "Usa export named quando il codice definisce una funzione o una classe pubblica.";
251
 
252
  FSH.refactor =
253
  "TECNICA OBBLIGATORIA: guard clauses + early return.\n" +
 
2443
  const reportPath=`/tmp/agente-ai/benchmark-v5-${seed}.json`;
2444
  const report={
2445
  timestamp:new Date().toISOString(),version:"extended-v5",
2446
+ runner:"benchmark-extended.mjs",
2447
+ apiContract:"POST /api/agent/tasks + GET /api/agent/tasks/{taskId}/stream",
2448
+ sseParser:"normalizeSSEEvent",
2449
+ codeExtractor:"extractCode-v2",
2450
  seed,replayCli:`node benchmark-extended.mjs --seed ${seed}`,
2451
  spaceVersion:spaceV,
2452
  specCoverage:{A_ragionamento:"reasoning",B_orchestrazione:"orchestration",
 
2593
  : null,
2594
  avgDevin: _runResult.avgDevin,
2595
  avgManus: _runResult.avgManus,
2596
+ verdict: _runResult.avgScore == null ? "NON_VALUTABILE" : _runResult.avgScore >= _runResult.avgReplit ? "PARI_REPLIT" : "SOTTO_REPLIT",
2597
  };
2598
+ saveBenchmarkReport("benchmark-extended-single", {
2599
+ summary,
2600
+ tasks: _runResult.tasks ?? [],
2601
+ taskFailures: _runResult.taskFailures ?? [],
2602
+ });
2603
  } catch (e) { console.log(`${R}⚠️ Report engine error: ${e.message}${NC}`); }
2604
 
2605
  if(F_IMPROVE){ await runImprovementCycle(_runResult,_runResult.selectedTasks); }
main.py CHANGED
@@ -145,6 +145,9 @@ _ROUTER_MAP = {
145
  "skills": "skills",
146
  "private_state": "private_state",
147
  "auth": "auth_managed",
 
 
 
148
  # ── Aggiunti ROUTER-COMPLETE (29 moduli orfani rimontati) ─────────────────
149
  "agent_checkpoint": "agent_checkpoint",
150
  "agent_telemetry": "agent_telemetry",
 
145
  "skills": "skills",
146
  "private_state": "private_state",
147
  "auth": "auth_managed",
148
+ "public_status": "public_status",
149
+ "me_tasks": "me_tasks",
150
+ "admin_state": "admin_state",
151
  # ── Aggiunti ROUTER-COMPLETE (29 moduli orfani rimontati) ─────────────────
152
  "agent_checkpoint": "agent_checkpoint",
153
  "agent_telemetry": "agent_telemetry",
tests/test_public_personal_boundary.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from api.me_tasks import TaskCreate, TaskUpdate, _owner
4
+ from api.public_status import _PUBLIC_FIELDS
5
+
6
+ ROOT = Path(__file__).resolve().parents[2]
7
+ MIGRATION = ROOT / "supabase/migrations/20260822_public_personal_state_phase1.sql"
8
+
9
+
10
+ def test_public_status_whitelist_contains_no_operational_columns():
11
+ assert "service_status" in _PUBLIC_FIELDS
12
+ assert "active_sessions" in _PUBLIC_FIELDS
13
+ assert "context" not in _PUBLIC_FIELDS
14
+ assert "goal" not in _PUBLIC_FIELDS
15
+ assert "agent_tasks" not in _PUBLIC_FIELDS
16
+
17
+
18
+ def test_personal_task_owner_is_derived_from_verified_user():
19
+ assert _owner({"id": "user-a", "user_metadata": {"id": "user-b"}}) == "user-a"
20
+
21
+
22
+ def test_task_models_boundaries():
23
+ task = TaskCreate(goal=" test goal ")
24
+ assert task.goal == " test goal "
25
+ assert TaskUpdate(status="cancelled").status == "cancelled"
26
+
27
+
28
+ def test_migration_keeps_operational_table_closed_to_client_roles():
29
+ sql = MIGRATION.read_text()
30
+ sql_without_comments = "\n".join(line for line in sql.splitlines() if not line.lstrip().startswith("--"))
31
+ assert "public.agent_tasks" not in sql_without_comments
32
+ assert "REVOKE ALL PRIVILEGES ON TABLE public.user_agent_tasks FROM PUBLIC, anon, authenticated" in sql
33
+ assert "GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.user_agent_tasks TO authenticated" in sql
34
+ assert "CREATE POLICY \"user_agent_tasks_read_own\"" in sql
35
+ assert "CREATE TABLE IF NOT EXISTS public.public_dashboard_snapshot" in sql
36
+
37
+
38
+ def test_admin_role_uses_app_metadata_not_user_metadata():
39
+ source = (ROOT / "backend/api/auth_guard.py").read_text()
40
+ assert "app_metadata" in source
41
+ admin_source = source[source.index("async def require_admin_user"):source.index("class AuthRole")]
42
+ assert "user.get(\"user_metadata\")" not in admin_source