Baida00 commited on
Commit
e6d624d
Β·
1 Parent(s): 1995f11

fix: move files to root and update Dockerfile

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. Dockerfile +6 -5
  2. README.md +36 -5
  3. {backend/agents β†’ agents}/__init__.py +0 -0
  4. {backend/agents β†’ agents}/acceptance_criteria.py +0 -0
  5. {backend/agents β†’ agents}/backend_antiregress.py +0 -0
  6. {backend/agents β†’ agents}/context_manager.py +0 -0
  7. {backend/agents β†’ agents}/critic.py +0 -0
  8. {backend/agents β†’ agents}/dynamic_replanner.py +0 -0
  9. {backend/agents β†’ agents}/error_classifier.py +0 -0
  10. {backend/agents β†’ agents}/escalation_ladder.py +0 -0
  11. {backend/agents β†’ agents}/executor.py +0 -0
  12. {backend/agents β†’ agents}/goal_drift_detector.py +0 -0
  13. {backend/agents β†’ agents}/goal_verifier.py +0 -0
  14. {backend/agents β†’ agents}/planner.py +0 -0
  15. {backend/agents β†’ agents}/reasoning_core.py +0 -0
  16. {backend/agents β†’ agents}/requirement_engine.py +0 -0
  17. {backend/agents β†’ agents}/response_verifier.py +0 -0
  18. {backend/agents β†’ agents}/skill_tracker.py +0 -0
  19. {backend/agents β†’ agents}/strategic_healer.py +0 -0
  20. {backend/agents β†’ agents}/tdd_runner.py +0 -0
  21. {backend/agents β†’ agents}/tool_generator.py +0 -0
  22. agents/unified_loop.py +0 -27
  23. {backend/agents β†’ agents}/unified_loop_helpers.py +0 -0
  24. {backend/agents β†’ agents}/unified_loop_llm.py +0 -0
  25. {backend/agents β†’ agents}/unified_loop_prompts.py +0 -0
  26. {backend/agents β†’ agents}/unified_loop_tools.py +0 -0
  27. {backend/agents β†’ agents}/unified_loop_types.py +0 -0
  28. {backend/api β†’ api}/__init__.py +0 -0
  29. {backend/api β†’ api}/agent.py +0 -0
  30. {backend/api β†’ api}/agent_memory.py +0 -0
  31. {backend/api β†’ api}/auth_guard.py +0 -0
  32. {backend/api β†’ api}/auth_managed.py +0 -0
  33. {backend/api β†’ api}/benchmark.py +0 -0
  34. {backend/api β†’ api}/benchmark_handler.py +0 -0
  35. {backend/api β†’ api}/blackboard.py +0 -0
  36. {backend/api β†’ api}/browser.py +0 -0
  37. {backend/api β†’ api}/coding.py +0 -0
  38. {backend/api β†’ api}/conversations.py +0 -0
  39. {backend/api β†’ api}/daemon_status.py +0 -0
  40. {backend/api β†’ api}/database.py +0 -0
  41. {backend/api β†’ api}/decision_memory.py +0 -0
  42. {backend/api β†’ api}/deploy.py +0 -0
  43. {backend/api β†’ api}/email.py +0 -0
  44. api/exec.py +85 -112
  45. {backend/api β†’ api}/exec_sandbox.py +0 -0
  46. {backend/api β†’ api}/files.py +0 -0
  47. {backend/api β†’ api}/gemini_vision.py +0 -0
  48. {backend/api β†’ api}/incident_registry.py +0 -0
  49. {backend/api β†’ api}/integrity_manager.py +0 -0
  50. {backend/api β†’ api}/linter.py +0 -0
Dockerfile CHANGED
@@ -3,7 +3,7 @@ FROM python:3.11-slim
3
  ENV PYTHONUNBUFFERED=1 \
4
  PYTHONDONTWRITEBYTECODE=1 \
5
  PORT=7860 \
6
- PYTHONPATH=/home/user/app \
7
  PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
8
 
9
  WORKDIR /app
@@ -15,12 +15,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
15
  libxfixes3 libxrandr2 libgbm1 libasound2 \
16
  && rm -rf /var/lib/apt/lists/*
17
 
18
- # Space has backend/ subdir (full repo push) β€” copy from backend/
19
- COPY backend/requirements.txt /app/requirements.txt
20
  RUN pip install --no-cache-dir -r /app/requirements.txt \
21
  && playwright install chromium \
22
  && chmod -R 755 /ms-playwright
23
 
 
24
  ARG TTYD_VERSION=1.7.7
25
  RUN ARCH=$(uname -m) && \
26
  TTYD_ARCH=$([ "$ARCH" = "aarch64" ] && echo "aarch64" || echo "x86_64") && \
@@ -33,8 +34,8 @@ USER user
33
  ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH
34
 
35
  WORKDIR /home/user/app
36
- # Copy only backend/ contents β†’ main.py lands at /home/user/app/main.py
37
- COPY --chown=user backend/ /home/user/app/
38
 
39
  EXPOSE 7860
 
40
  CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860} --workers 1"]
 
3
  ENV PYTHONUNBUFFERED=1 \
4
  PYTHONDONTWRITEBYTECODE=1 \
5
  PORT=7860 \
6
+ FRONTEND_DIST=/home/user/app/static \
7
  PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
8
 
9
  WORKDIR /app
 
15
  libxfixes3 libxrandr2 libgbm1 libasound2 \
16
  && rm -rf /var/lib/apt/lists/*
17
 
18
+ # hf-sync copia backend/* nella root dello Space β€” nessun prefisso backend/
19
+ COPY requirements.txt /app/requirements.txt
20
  RUN pip install --no-cache-dir -r /app/requirements.txt \
21
  && playwright install chromium \
22
  && chmod -R 755 /ms-playwright
23
 
24
+ # S356: ttyd β€” terminale web per accesso da iPhone Safari (binario statico, zero dep)
25
  ARG TTYD_VERSION=1.7.7
26
  RUN ARCH=$(uname -m) && \
27
  TTYD_ARCH=$([ "$ARCH" = "aarch64" ] && echo "aarch64" || echo "x86_64") && \
 
34
  ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH
35
 
36
  WORKDIR /home/user/app
37
+ COPY --chown=user . /home/user/app/
 
38
 
39
  EXPOSE 7860
40
+
41
  CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860} --workers 1"]
README.md CHANGED
@@ -1,11 +1,42 @@
1
  ---
2
- title: AI Backend Collab
3
  emoji: πŸ€–
4
- colorFrom: blue
5
- colorTo: green
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- # AI Backend Collab
11
- Backend di failover per il progetto Agente AI.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Agente AI Backend
3
  emoji: πŸ€–
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # Agente AI β€” Backend FastAPI
12
+
13
+ Backend Python FastAPI per Agente AI. Streaming LLM, memoria, esecuzione codice, terminal PTY.
14
+
15
+ ## Endpoints principali
16
+
17
+ - `GET /health` β€” stato backend
18
+ - `GET /api/status` β€” versione + config
19
+ - `POST /api/reason/loop` β€” agent reasoning loop
20
+ - `POST /api/exec` β€” esecuzione codice Python
21
+ - `POST /api/execute-shell` β€” shell commands
22
+ - `POST /api/search` β€” web search proxy
23
+ - `POST /api/fetch-page` β€” page fetch proxy
24
+ - `WS /ws/terminal` β€” PTY WebSocket terminal
25
+
26
+ ## Stack
27
+
28
+ - Python 3.11 + FastAPI + uvicorn
29
+ - smolagents>=1.14.0 + litellm>=1.40.0
30
+ - supabase (opzionale)
31
+
32
+ ## Variabili ambiente
33
+
34
+ | Variabile | Descrizione |
35
+ |-----------|-------------|
36
+ | GROQ_API_KEY | Groq API key |
37
+ | GEMINI_API_KEY | Google Gemini key |
38
+ | OPENROUTER_API_KEY | OpenRouter key |
39
+ | HF_TOKEN | HuggingFace token |
40
+ | SUPABASE_URL | Supabase URL (opzionale) |
41
+ | SUPABASE_ANON_KEY | Supabase anon key (opzionale) |
42
+ | ALLOWED_ORIGINS | CORS origins comma-separated |
{backend/agents β†’ agents}/__init__.py RENAMED
File without changes
{backend/agents β†’ agents}/acceptance_criteria.py RENAMED
File without changes
{backend/agents β†’ agents}/backend_antiregress.py RENAMED
File without changes
{backend/agents β†’ agents}/context_manager.py RENAMED
File without changes
{backend/agents β†’ agents}/critic.py RENAMED
File without changes
{backend/agents β†’ agents}/dynamic_replanner.py RENAMED
File without changes
{backend/agents β†’ agents}/error_classifier.py RENAMED
File without changes
{backend/agents β†’ agents}/escalation_ladder.py RENAMED
File without changes
{backend/agents β†’ agents}/executor.py RENAMED
File without changes
{backend/agents β†’ agents}/goal_drift_detector.py RENAMED
File without changes
{backend/agents β†’ agents}/goal_verifier.py RENAMED
File without changes
{backend/agents β†’ agents}/planner.py RENAMED
File without changes
{backend/agents β†’ agents}/reasoning_core.py RENAMED
File without changes
{backend/agents β†’ agents}/requirement_engine.py RENAMED
File without changes
{backend/agents β†’ agents}/response_verifier.py RENAMED
File without changes
{backend/agents β†’ agents}/skill_tracker.py RENAMED
File without changes
{backend/agents β†’ agents}/strategic_healer.py RENAMED
File without changes
{backend/agents β†’ agents}/tdd_runner.py RENAMED
File without changes
{backend/agents β†’ agents}/tool_generator.py RENAMED
File without changes
agents/unified_loop.py CHANGED
@@ -1660,19 +1660,6 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
1660
  exec_warn.insert(0, self._tdd_fail_inject)
1661
  _logger.info("GAP-NEW-2: TDD fail iniettato in exec_warn (%d chars)", len(self._tdd_fail_inject))
1662
  self._tdd_fail_inject = None
1663
- # GAP-4: StrategicHealer β€” analisi LLM pattern di fallimento (integra GAP-SELFHEAL v2)
1664
- if exec_errors and getattr(self, '_strategic_healer', None):
1665
- try:
1666
- _sh_ctx_str = "\n".join(str(w) for w in exec_warn[-10:] if isinstance(w, str))
1667
- _sh_decision = await self._strategic_healer.analyze_and_decide(exec_errors, _sh_ctx_str)
1668
- if _sh_decision and getattr(_sh_decision, 'strategy_prompt', None):
1669
- exec_warn.insert(0, _sh_decision.strategy_prompt)
1670
- _logger.info("GAP-4: StrategicHealer strategy iniettata in exec_warn")
1671
- if _sh_decision and getattr(_sh_decision, 'should_stop', False):
1672
- _logger.info("GAP-4: StrategicHealer β†’ should_stop, interruzione loop")
1673
- break
1674
- except Exception as _sh_loop_err:
1675
- _logger.debug("GAP-4: StrategicHealer loop silenced β€” %s", _sh_loop_err)
1676
  # GAP-SELFHEAL v2: dual-mode fingerprinting β€” raw + error-class extraction.
1677
  # PROBLEMA v1: MD5("ModuleNotFoundError: requests") β‰  MD5("ModuleNotFoundError: pandas")
1678
  # β†’ 3 librerie diverse con stesso errore NON triggheravano il cambio strategia.
@@ -3420,20 +3407,6 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
3420
 
3421
  state = UnifiedLoopState(goal=goal, context=context, max_steps=max_steps, session_id=session_id)
3422
 
3423
- # GAP-4: StrategicHealer β€” init + load past failures (LLM-based self-healing cognitivo)
3424
- try:
3425
- from agents.strategic_healer import StrategicHealer as _SHClass
3426
- self._strategic_healer = _SHClass(
3427
- getattr(self, 'llm', None) or getattr(self, '_llm', None),
3428
- state.goal,
3429
- memory=getattr(self, 'memory', None) or getattr(self, '_memory', None)
3430
- )
3431
- await self._strategic_healer.load_past_failures()
3432
- _logger.info("GAP-4: StrategicHealer inizializzato per goal '%s'", state.goal[:60])
3433
- except Exception as _sh_init_err:
3434
- self._strategic_healer = None
3435
- _logger.debug("GAP-4: StrategicHealer init silenced β€” %s", _sh_init_err)
3436
-
3437
  # P17-F2: inject blackboard critical entries at loop start.
3438
  # I delegate frontend scrivono su Upstash; il loop legge e inietta nel context.
3439
  if session_id:
 
1660
  exec_warn.insert(0, self._tdd_fail_inject)
1661
  _logger.info("GAP-NEW-2: TDD fail iniettato in exec_warn (%d chars)", len(self._tdd_fail_inject))
1662
  self._tdd_fail_inject = None
 
 
 
 
 
 
 
 
 
 
 
 
 
1663
  # GAP-SELFHEAL v2: dual-mode fingerprinting β€” raw + error-class extraction.
1664
  # PROBLEMA v1: MD5("ModuleNotFoundError: requests") β‰  MD5("ModuleNotFoundError: pandas")
1665
  # β†’ 3 librerie diverse con stesso errore NON triggheravano il cambio strategia.
 
3407
 
3408
  state = UnifiedLoopState(goal=goal, context=context, max_steps=max_steps, session_id=session_id)
3409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3410
  # P17-F2: inject blackboard critical entries at loop start.
3411
  # I delegate frontend scrivono su Upstash; il loop legge e inietta nel context.
3412
  if session_id:
{backend/agents β†’ agents}/unified_loop_helpers.py RENAMED
File without changes
{backend/agents β†’ agents}/unified_loop_llm.py RENAMED
File without changes
{backend/agents β†’ agents}/unified_loop_prompts.py RENAMED
File without changes
{backend/agents β†’ agents}/unified_loop_tools.py RENAMED
File without changes
{backend/agents β†’ agents}/unified_loop_types.py RENAMED
File without changes
{backend/api β†’ api}/__init__.py RENAMED
File without changes
{backend/api β†’ api}/agent.py RENAMED
File without changes
{backend/api β†’ api}/agent_memory.py RENAMED
File without changes
{backend/api β†’ api}/auth_guard.py RENAMED
File without changes
{backend/api β†’ api}/auth_managed.py RENAMED
File without changes
{backend/api β†’ api}/benchmark.py RENAMED
File without changes
{backend/api β†’ api}/benchmark_handler.py RENAMED
File without changes
{backend/api β†’ api}/blackboard.py RENAMED
File without changes
{backend/api β†’ api}/browser.py RENAMED
File without changes
{backend/api β†’ api}/coding.py RENAMED
File without changes
{backend/api β†’ api}/conversations.py RENAMED
File without changes
{backend/api β†’ api}/daemon_status.py RENAMED
File without changes
{backend/api β†’ api}/database.py RENAMED
File without changes
{backend/api β†’ api}/decision_memory.py RENAMED
File without changes
{backend/api β†’ api}/deploy.py RENAMED
File without changes
{backend/api β†’ api}/email.py RENAMED
File without changes
api/exec.py CHANGED
@@ -5,20 +5,6 @@ import ast as _ast_mod
5
  from fastapi import APIRouter, Depends, HTTPException, Request
6
  from pydantic import BaseModel, model_validator
7
  from .auth_guard import require_role, AuthRole
8
- try:
9
- from .priority import realtime_job as _realtime_job, background_job as _background_job
10
- except ImportError:
11
- # Fallback graceful se priority.py non ancora deployato
12
- from contextlib import asynccontextmanager
13
- import asyncio as _asyncio_fallback
14
- _FALLBACK_REALTIME_SEM = _asyncio_fallback.Semaphore(6)
15
- _FALLBACK_BACKGROUND_SEM = _asyncio_fallback.Semaphore(2)
16
- @asynccontextmanager
17
- async def _realtime_job(**_):
18
- async with _FALLBACK_REALTIME_SEM: yield
19
- @asynccontextmanager
20
- async def _background_job(**_):
21
- async with _FALLBACK_BACKGROUND_SEM: yield
22
 
23
  import logging
24
  _logger = logging.getLogger("api.exec")
@@ -287,63 +273,59 @@ async def exec_code(req: ExecRequest, request: Request):
287
  return {'stdout': '', 'stderr': f'Blocked (AST): {_ast_reason}', 'exit_code': 1, 'durationMs': 0}
288
 
289
  t0 = int(time.time() * 1000)
290
- try:
291
- async with _realtime_job(timeout_s=120.0):
292
- with tempfile.TemporaryDirectory() as tmpdir:
293
- try:
294
- if lang == 'python':
295
- cmd = [_get_venv_python(), '-c', code]
296
- elif lang in ('javascript', 'js'):
297
- cmd = ['node', '-e', code]
298
- elif lang in ('typescript', 'ts'):
299
- fname = os.path.join(tmpdir, 'snippet.ts')
300
- with open(fname, 'w') as f:
301
- f.write(code)
302
- cmd = ['npx', '--yes', 'ts-node', '--transpile-only', fname]
303
- else:
304
- return {'stdout': '', 'stderr': f'Unsupported lang: {lang}', 'exit_code': 1, 'durationMs': 0}
305
-
306
- proc = await asyncio.create_subprocess_exec(
307
- *cmd,
308
- stdout=asyncio.subprocess.PIPE,
309
- stderr=asyncio.subprocess.PIPE,
310
- cwd=tmpdir,
311
- preexec_fn=_child_resource_limits, # GAP-EXEC-FIX: RLIMIT_AS/CPU/NOFILE/NPROC
312
- env={
313
- 'HOME': tmpdir, 'TMPDIR': tmpdir, 'NODE_ENV': 'production',
314
- 'PATH': os.environ.get('PATH', '/usr/local/bin:/usr/bin:/bin'),
315
- # NPM-CACHE: usa /data/npm-cache persistente β€” riduce re-download
316
- 'npm_config_cache': '/data/npm-cache',
317
- # NODE-MEM: limita heap V8 a 384MB per stare in Railway 512MB
318
- 'NODE_OPTIONS': '--max-old-space-size=384',
319
- },
320
- )
321
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15)
322
- return {
323
- 'stdout': stdout.decode('utf-8', errors='replace')[:8000],
324
- 'stderr': stderr.decode('utf-8', errors='replace')[:4000],
325
- 'exit_code': proc.returncode,
326
- 'durationMs': int(time.time() * 1000) - t0,
327
- }
328
- except asyncio.TimeoutError:
329
- # S758-ProgExec: capture partial stdout before kill β€” progressive execution
330
- _partial_out, _partial_err = b'', b''
331
- try:
332
- _killpg(proc) # P40-A: kill intero process group
333
- _partial_out, _partial_err = await asyncio.wait_for(proc.communicate(), timeout=2)
334
- except Exception as _exc:
335
- _logger.debug("[exec] silenced %s", type(_exc).__name__) # noqa: BLE001
336
- return {
337
- 'stdout': _partial_out.decode('utf-8', errors='replace')[:8000],
338
- 'stderr': f'⚠️ Timeout 15s β€” output parziale\n' + _partial_err.decode('utf-8', errors='replace')[:2000],
339
- 'exit_code': -1,
340
- 'durationMs': 15000,
341
- 'partial': True,
342
- }
343
- except Exception as e:
344
- return {'stdout': '', 'stderr': str(e), 'exit_code': -1, 'durationMs': int(time.time() * 1000) - t0}
345
- except asyncio.TimeoutError:
346
- return {'stdout': '', 'stderr': '⚠️ Nessun slot exec disponibile (server occupato). Riprova tra qualche secondo.', 'exit_code': -1, 'durationMs': int(time.time() * 1000) - t0}
347
 
348
 
349
  @router.post('/api/execute-shell')
@@ -356,42 +338,38 @@ async def execute_shell(cmd: ShellCmd, request: Request):
356
  if bad in raw:
357
  raise HTTPException(400, 'Command blocked for safety')
358
  timeout = min(max(cmd.timeout, 1), 60)
359
- try:
360
- async with _realtime_job(timeout_s=90.0):
361
- with tempfile.TemporaryDirectory() as tmpdir:
362
- try:
363
- proc = await asyncio.create_subprocess_shell(
364
- raw,
365
- stdout=asyncio.subprocess.PIPE,
366
- stderr=asyncio.subprocess.PIPE,
367
- cwd=tmpdir,
368
- preexec_fn=_child_resource_limits, # GAP-EXEC-FIX: RLIMIT_AS/CPU/NOFILE/NPROC
369
- env={**os.environ, 'HOME': tmpdir, 'TMPDIR': tmpdir},
370
- )
371
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
372
- return {
373
- 'stdout': stdout.decode('utf-8', errors='replace')[:8000],
374
- 'stderr': stderr.decode('utf-8', errors='replace')[:4000],
375
- 'exit_code': proc.returncode,
376
- }
377
- except asyncio.TimeoutError:
378
- # S758-ProgExec: capture partial stdout before kill β€” progressive execution
379
- _partial_out, _partial_err = b'', b''
380
- try:
381
- _killpg(proc) # P40-A: kill intero process group
382
- _partial_out, _partial_err = await asyncio.wait_for(proc.communicate(), timeout=2)
383
- except Exception as _exc:
384
- _logger.debug("[exec] silenced %s", type(_exc).__name__) # noqa: BLE001
385
- return {
386
- 'stdout': _partial_out.decode('utf-8', errors='replace')[:8000],
387
- 'stderr': f'⚠️ Timeout {timeout}s β€” output parziale\n' + _partial_err.decode('utf-8', errors='replace')[:2000],
388
- 'exit_code': -1,
389
- 'partial': True,
390
- }
391
- except Exception as e:
392
- return {'stdout': '', 'stderr': str(e), 'exit_code': -1}
393
- except asyncio.TimeoutError:
394
- return {'stdout': '', 'stderr': '⚠️ Nessun slot shell disponibile. Riprova tra qualche secondo.', 'exit_code': -1}
395
 
396
 
397
  @router.post('/api/pip-install')
@@ -407,11 +385,6 @@ async def pip_install(
407
  for p in pkgs:
408
  if not safe.match(p):
409
  raise HTTPException(400, f'Invalid package name: {p}')
410
- try:
411
- async with _background_job(timeout_s=30.0):
412
- pass # slot acquisito β€” pip gira fuori dal semaphore (subprocess indipendente)
413
- except asyncio.TimeoutError:
414
- raise HTTPException(429, 'Server occupato con altri job pesanti. Riprova tra 30s.')
415
  # CHUNKED-PIP: installa in batch da 3 pacchetti β€” previene OOM su Railway free.
416
  # Ogni batch ha timeout 45s indipendente: un batch lento non blocca i successivi.
417
  # pip cache su /data/pip-cache (persistente tra restart HF Space / Railway).
 
5
  from fastapi import APIRouter, Depends, HTTPException, Request
6
  from pydantic import BaseModel, model_validator
7
  from .auth_guard import require_role, AuthRole
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  import logging
10
  _logger = logging.getLogger("api.exec")
 
273
  return {'stdout': '', 'stderr': f'Blocked (AST): {_ast_reason}', 'exit_code': 1, 'durationMs': 0}
274
 
275
  t0 = int(time.time() * 1000)
276
+ with tempfile.TemporaryDirectory() as tmpdir:
277
+ try:
278
+ if lang == 'python':
279
+ cmd = [_get_venv_python(), '-c', code]
280
+ elif lang in ('javascript', 'js'):
281
+ cmd = ['node', '-e', code]
282
+ elif lang in ('typescript', 'ts'):
283
+ fname = os.path.join(tmpdir, 'snippet.ts')
284
+ with open(fname, 'w') as f:
285
+ f.write(code)
286
+ cmd = ['npx', '--yes', 'ts-node', '--transpile-only', fname]
287
+ else:
288
+ return {'stdout': '', 'stderr': f'Unsupported lang: {lang}', 'exit_code': 1, 'durationMs': 0}
289
+
290
+ proc = await asyncio.create_subprocess_exec(
291
+ *cmd,
292
+ stdout=asyncio.subprocess.PIPE,
293
+ stderr=asyncio.subprocess.PIPE,
294
+ cwd=tmpdir,
295
+ preexec_fn=_child_resource_limits, # GAP-EXEC-FIX: RLIMIT_AS/CPU/NOFILE/NPROC
296
+ env={
297
+ 'HOME': tmpdir, 'TMPDIR': tmpdir, 'NODE_ENV': 'production',
298
+ 'PATH': os.environ.get('PATH', '/usr/local/bin:/usr/bin:/bin'),
299
+ # NPM-CACHE: usa /data/npm-cache persistente β€” riduce re-download
300
+ 'npm_config_cache': '/data/npm-cache',
301
+ # NODE-MEM: limita heap V8 a 384MB per stare in Railway 512MB
302
+ 'NODE_OPTIONS': '--max-old-space-size=384',
303
+ },
304
+ )
305
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15)
306
+ return {
307
+ 'stdout': stdout.decode('utf-8', errors='replace')[:8000],
308
+ 'stderr': stderr.decode('utf-8', errors='replace')[:4000],
309
+ 'exit_code': proc.returncode,
310
+ 'durationMs': int(time.time() * 1000) - t0,
311
+ }
312
+ except asyncio.TimeoutError:
313
+ # S758-ProgExec: capture partial stdout before kill β€” progressive execution
314
+ _partial_out, _partial_err = b'', b''
315
+ try:
316
+ _killpg(proc) # P40-A: kill intero process group
317
+ _partial_out, _partial_err = await asyncio.wait_for(proc.communicate(), timeout=2)
318
+ except Exception as _exc:
319
+ _logger.debug("[exec] silenced %s", type(_exc).__name__) # noqa: BLE001
320
+ return {
321
+ 'stdout': _partial_out.decode('utf-8', errors='replace')[:8000],
322
+ 'stderr': f'⚠️ Timeout 15s β€” output parziale\n' + _partial_err.decode('utf-8', errors='replace')[:2000],
323
+ 'exit_code': -1,
324
+ 'durationMs': 15000,
325
+ 'partial': True,
326
+ }
327
+ except Exception as e:
328
+ return {'stdout': '', 'stderr': str(e), 'exit_code': -1, 'durationMs': int(time.time() * 1000) - t0}
 
 
 
 
329
 
330
 
331
  @router.post('/api/execute-shell')
 
338
  if bad in raw:
339
  raise HTTPException(400, 'Command blocked for safety')
340
  timeout = min(max(cmd.timeout, 1), 60)
341
+ with tempfile.TemporaryDirectory() as tmpdir:
342
+ try:
343
+ proc = await asyncio.create_subprocess_shell(
344
+ raw,
345
+ stdout=asyncio.subprocess.PIPE,
346
+ stderr=asyncio.subprocess.PIPE,
347
+ cwd=tmpdir,
348
+ preexec_fn=_child_resource_limits, # GAP-EXEC-FIX: RLIMIT_AS/CPU/NOFILE/NPROC
349
+ env={**os.environ, 'HOME': tmpdir, 'TMPDIR': tmpdir},
350
+ )
351
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
352
+ return {
353
+ 'stdout': stdout.decode('utf-8', errors='replace')[:8000],
354
+ 'stderr': stderr.decode('utf-8', errors='replace')[:4000],
355
+ 'exit_code': proc.returncode,
356
+ }
357
+ except asyncio.TimeoutError:
358
+ # S758-ProgExec: capture partial stdout before kill β€” progressive execution
359
+ _partial_out, _partial_err = b'', b''
360
+ try:
361
+ _killpg(proc) # P40-A: kill intero process group
362
+ _partial_out, _partial_err = await asyncio.wait_for(proc.communicate(), timeout=2)
363
+ except Exception as _exc:
364
+ _logger.debug("[exec] silenced %s", type(_exc).__name__) # noqa: BLE001
365
+ return {
366
+ 'stdout': _partial_out.decode('utf-8', errors='replace')[:8000],
367
+ 'stderr': f'⚠️ Timeout {timeout}s β€” output parziale\n' + _partial_err.decode('utf-8', errors='replace')[:2000],
368
+ 'exit_code': -1,
369
+ 'partial': True,
370
+ }
371
+ except Exception as e:
372
+ return {'stdout': '', 'stderr': str(e), 'exit_code': -1}
 
 
 
 
373
 
374
 
375
  @router.post('/api/pip-install')
 
385
  for p in pkgs:
386
  if not safe.match(p):
387
  raise HTTPException(400, f'Invalid package name: {p}')
 
 
 
 
 
388
  # CHUNKED-PIP: installa in batch da 3 pacchetti β€” previene OOM su Railway free.
389
  # Ogni batch ha timeout 45s indipendente: un batch lento non blocca i successivi.
390
  # pip cache su /data/pip-cache (persistente tra restart HF Space / Railway).
{backend/api β†’ api}/exec_sandbox.py RENAMED
File without changes
{backend/api β†’ api}/files.py RENAMED
File without changes
{backend/api β†’ api}/gemini_vision.py RENAMED
File without changes
{backend/api β†’ api}/incident_registry.py RENAMED
File without changes
{backend/api β†’ api}/integrity_manager.py RENAMED
File without changes
{backend/api β†’ api}/linter.py RENAMED
File without changes