Skip to content

security: auditoria completa 0.8 — vulnerabilitats, hardening i tests#2

Merged
jgoy-labs merged 1 commit into
mainfrom
pre-production
Feb 21, 2026
Merged

security: auditoria completa 0.8 — vulnerabilitats, hardening i tests#2
jgoy-labs merged 1 commit into
mainfrom
pre-production

Conversation

@jgoy-labs

@jgoy-labs jgoy-labs commented Feb 21, 2026

Copy link
Copy Markdown
Owner

Resum

Implementació completa del pla d'auditoria de seguretat per Nexe Server 0.8. S'han analitzat ~11.000 fitxers Python i s'han corregit 21 problemes classificats per severitat.

🔴 Crítiques resoltes

  • C-1 — Secrets NEXE_PRIMARY_API_KEY i NEXE_CSRF_SECRET rotats
  • C-3 — Bare except: substituïts per except Exception as e: amb logging a 4 fitxers

🟠 Altes resoltes

  • A-1 — Upload path traversal: sanititzar filename + whitelist d'extensions ({.txt, .md, .pdf, .csv, .rst, .html})
  • A-2 — Qdrant: suport QDRANT_API_KEY per entorns remots
  • A-3/health/ready: retorna {status, timestamp} sense exposar llista de mòduls sense auth
  • A-4 — HTTPException 500: eliminar detail=str(e) (no filtrar internals als clients)
  • A-5 — Qdrant log file: flush() + = None al shutdown
  • A-6SessionManager.cleanup_inactive() implementat amb TTL real
  • A-7 — Streaming Ollama: gestionar asyncio.CancelledError (client desconnectat)

🟡 Mitges resoltes

  • M-2 — Docker healthcheck per al servei nexe a docker-compose.yml
  • M-3qdrant/qdrant:latestqdrant/qdrant:v1.12.0
  • M-4.dockerignore creat (exclou .env, storage/, .git/, tests/)
  • M-5httpx eliminat de requirements-dev.txt (duplicat)
  • M-6pip-audit afegit al CI pipeline

🟢 Tests de seguretat (14/14 passing)

Nou fitxer tests/integration/test_security.py:

  • Path traversal i extensions no permeses a upload
  • Regressió _sanitize_rag_context (truncació + filtratge injeccions)
  • /health/ready no exposa informació de mòduls sense auth
  • Session TTL cleanup

Altres millores incloses a la branca

  • Dockerfile: usuari non-root nexe:nexe, permisos 750
  • runner.py: validate_production_config() per detectar secrets buits en producció
  • scripts/generate_secrets.sh: script per generar secrets segurs
  • knowledge/SECURITY.md, DEPLOYMENT.md: documentació de seguretat

Pla de tests

  • pytest tests/integration/test_security.py -v → 14/14 passing
  • pip-audit -r requirements.txt → sense CVEs crítics
  • docker compose up --build -d → healthcheck OK per ambdós serveis
  • Verificar que /health/ready retorna {status, timestamp} sense detalls de mòduls
  • Verificar que upload de .exe retorna HTTP 400
  • Verificar que upload de ../../etc/passwd retorna HTTP 400

Assisted by AI

…cepts

- memory/rag/routers/endpoints.py: ALLOWED_UPLOAD_EXTENSIONS whitelist,
  filename sanitization (Path.name) to prevent path traversal,
  generic 500 error messages (no internal details exposed)
- core/endpoints/root.py: /health/ready returns only {status, timestamp}
  to avoid exposing internal module list without auth
- plugins/web_ui_module/session_manager.py: implement cleanup_inactive()
  with timedelta TTL to prevent session memory leak
- memory/memory/engines/persistence.py: QDRANT_API_KEY env var support
  for authenticated Qdrant deployments
- core/endpoints/chat.py: log JSONDecodeError in Ollama stream,
  handle asyncio.CancelledError on client disconnect
- plugins/web_ui_module/memory_helper.py: replace bare except with
  except Exception + debug logging
- .env.example: document all env vars including QDRANT_API_KEY
- .dockerignore: exclude .env, storage/, venv/, .git/ from Docker image
- .github/workflows/ci.yml: pip-audit CVE scan + unit tests
- core/endpoints/tests/test_security.py: 14 security regression tests

Assisted by AI
@jgoy-labs jgoy-labs merged commit 6574f63 into main Feb 21, 2026
0 of 2 checks passed
@jgoy-labs jgoy-labs deleted the pre-production branch February 21, 2026 15:34
jgoy-labs added a commit that referenced this pull request Mar 7, 2026
Issue 8 - {{NEXE_*}} templates substituïts per IDs literals:
- memory/embeddings: {{NEXE_EMBEDDINGS_MODULE}} -> 'embeddings'
- memory/memory: {{NEXE_MEMORY_MODULE}} -> 'memory', dependencies corregides
- memory/rag: {{NEXE_RAG_MODULE}} -> 'rag'
- plugins/security: {{NEXE_SECURITY_MODULE}} -> 'security'
- core/cli/manifest.toml: comentari corregit (cli)

Issues 9+10 - URLs hardcoded restants (lifespan.py + ollama_module):
- lifespan.py:96 unificat NEXE_OLLAMA_HOST > OLLAMA_HOST (fallback)
- lifespan.py:247,585 usen NEXE_OLLAMA_HOST
- lifespan.py:482,564,565 usen NEXE_API_BASE_URL / config
- ollama_module/module.py:420 usa NEXE_API_BASE_URL

Issue 11 - datetime.now() -> datetime.now(timezone.utc):
- core/endpoints/bootstrap.py, root.py
- plugins/security/core/logger.py
- plugins/web_ui_module/session_manager.py, memory_helper.py
- memory_helper.py: handle naive datetimes antics (replace tzinfo)
- test_security.py actualitzat per usar datetime.now(timezone.utc)

Issue 12 - Variables d'entorn sense prefix NEXE_ unificades:
- BOOTSTRAP_TTL -> NEXE_BOOTSTRAP_TTL (amb fallback a nom antic)
- AUTO_CLEAN_ENABLED -> NEXE_AUTO_CLEAN_ENABLED (idem)
- AUTO_CLEAN_DRY_RUN -> NEXE_AUTO_CLEAN_DRY_RUN (idem)
- OLLAMA_HOST mantingut com a fallback de NEXE_OLLAMA_HOST

Issue 13 - .env.example: variables que faltaven documentades:
- NEXE_BOOTSTRAP_DISPLAY, NEXE_BOOTSTRAP_TTL
- NEXE_AUTO_CLEAN_ENABLED, NEXE_AUTO_CLEAN_DRY_RUN
- NEXE_QDRANT_HEALTH_TIMEOUT
- OLLAMA_HOST (com a fallback comentat)

Tests: 954 passed, 0 failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jgoy-labs added a commit that referenced this pull request Mar 9, 2026
#3 loader: fallback només considera classes definides al mòdul
   (afegit attr.__module__ == module.__name__ per evitar classes importades)
#2 singletons: afegits reset_config() i reset_loader() per testing
#1 core/app.py: import os mogut al bloc d'imports del principi
#4 loader.py: docstrings traduïts de català a anglès (unificació open-source)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jgoy-labs added a commit that referenced this pull request Apr 12, 2026
Fix-All BUS sobre 3 tracks paral·lels per resoldre tots els bugs del QA
post-BUS de normalització abans del DMG v0.9.0. 8 commits dev consolidats
en aquest sync.

TRACK A — Memory/RAG/Sessions
- Bug #1 (PID file canònic) — single source of truth a storage/run/server.pid
- F5 — 3 col·leccions canòniques (nexe_web_ui, user_knowledge, nexe_documentation)
  creades a get_memory_api() en lloc de només la primera
- F7 — ingest_knowledge defaulteja a nexe_documentation (era user_knowledge)
  i és idempotent (eliminada la sequence delete_collection + create_collection
  destructiva que esborrava docs ad-hoc dels usuaris a cada install)
- F8 — root cause empíric Bug #4: MemoryModule obria un SEGON QdrantClient
  real a storage/vectors/qdrant_local/, divergent del singleton del pool.
  MEM_SAVE escrivia a una col·lecció, MEM_RECALL llegia d'una altra.
  Ara tots dos comparteixen storage/vectors/.
- F1 — _check_duplicate retorna contracte honest (success=False, duplicate=True)
  enlloc de fingir success=True amb document_id=None. Era el segon root cause
  de Bug #4: el dedup bloquejava SAVEs amb fals positius silenciats.
- F2 — typo cols list (nexe_web_ui duplicat)
- F3 — list_memories scroll-based (sense semantic search amb query anglesa)
- Bug #10 — collections= filter a list/save/delete (sidebar checks reals)
- Bug #6 — frontend hydration document attached. Eren 2 bugs encadenats:
  l'endpoint /history no retornava attached_document, i removeFilePreview()
  feia POST /clear-document destructiu cada switch de sessió.
- Bug #3 — MEM_SAVE-only response fallback. Quan el model emet només
  [MEM_SAVE: ...] sense text envoltant, ara genera 'Memòria desada: <fact>'
  perquè el bloc save s'executi i el frontend mostri confirmació.
- auto_save crida eliminada per HOMAD memoria v1 (2026-04-01) — manual
  MEM_SAVE only fins a Part 2.

TRACK B — Tray / Multi-instance / Packaging
- Bug #1 (PID file) compartit amb Track A
- Bug #2 — setproctitle a server i tray (server-nexe / nexe-tray a ps/Activity
  Monitor). Force Quit encara mostra Python perquè requereix CFBundleName via
  .app bundle real (deute v0.9.1).
- Bug #9 — menu polish: server-nexe.com duplicat substituït per
  '📖 Documentació' al main level (3 idiomes), website_item es manté al
  submenú Configuració.

TRACK C — UX cosmètic
- Bug #5 — slow_request middleware exclou /ui/upload (uploads naturalment
  triguen >1s i el log apareixia duplicat amb l'access log d'uvicorn).
- Bug #8 — 3 ⓘ visibles als checkboxes del sidebar de col·leccions amb
  tooltips als 3 idiomes (la infraestructura CSS/i18n ja existia).

Pytest D-1 final: 4424 passed, 0 failed, 35 skipped, 1 xfailed, 86% coverage
en 76.11s. Baseline pre-BUS era 4396. +28 tests nous, ZERO regressions.

Tests nous:
- tests/test_pid_file.py: 7 tests Bug #1
- tests/test_ingest_knowledge_idempotent.py: 8 tests F7 (3 classes)
- plugins/web_ui_module/tests/test_memory_helper_async.py: 1 test F1
- plugins/web_ui_module/tests/test_memory_delete.py: 7 tests F3+Bug#10
- plugins/web_ui_module/tests/test_mem_save_injection.py: 5 tests Bug #3

Out of scope (deute v0.9.1+):
- routes_chat.py 54KB decapitació general (deute formal P0)
- Bundle .app real amb py2app per CFBundleName (deute v0.9.1)
- Resums per capítol (Part 2 redisseny memory)
- RDBMS font de veritat + vector store reconstruïble (HOMAD memoria v1, Part 2)
jgoy-labs added a commit that referenced this pull request Apr 12, 2026
Fix-All BUS sobre 3 tracks paral·lels per resoldre tots els bugs del QA
post-BUS de normalització abans del DMG v0.9.0. 8 commits dev consolidats
en aquest sync.

TRACK A — Memory/RAG/Sessions
- Bug #1 (PID file canònic) — single source of truth a storage/run/server.pid
- F5 — 3 col·leccions canòniques (nexe_web_ui, user_knowledge, nexe_documentation)
  creades a get_memory_api() en lloc de només la primera
- F7 — ingest_knowledge defaulteja a nexe_documentation (era user_knowledge)
  i és idempotent (eliminada la sequence delete_collection + create_collection
  destructiva que esborrava docs ad-hoc dels usuaris a cada install)
- F8 — root cause empíric Bug #4: MemoryModule obria un SEGON QdrantClient
  real a storage/vectors/qdrant_local/, divergent del singleton del pool.
  MEM_SAVE escrivia a una col·lecció, MEM_RECALL llegia d'una altra.
  Ara tots dos comparteixen storage/vectors/.
- F1 — _check_duplicate retorna contracte honest (success=False, duplicate=True)
  enlloc de fingir success=True amb document_id=None. Era el segon root cause
  de Bug #4: el dedup bloquejava SAVEs amb fals positius silenciats.
- F2 — typo cols list (nexe_web_ui duplicat)
- F3 — list_memories scroll-based (sense semantic search amb query anglesa)
- Bug #10 — collections= filter a list/save/delete (sidebar checks reals)
- Bug #6 — frontend hydration document attached. Eren 2 bugs encadenats:
  l'endpoint /history no retornava attached_document, i removeFilePreview()
  feia POST /clear-document destructiu cada switch de sessió.
- Bug #3 — MEM_SAVE-only response fallback. Quan el model emet només
  [MEM_SAVE: ...] sense text envoltant, ara genera 'Memòria desada: <fact>'
  perquè el bloc save s'executi i el frontend mostri confirmació.
- auto_save crida eliminada per HOMAD memoria v1 (2026-04-01) — manual
  MEM_SAVE only fins a Part 2.

TRACK B — Tray / Multi-instance / Packaging
- Bug #1 (PID file) compartit amb Track A
- Bug #2 — setproctitle a server i tray (server-nexe / nexe-tray a ps/Activity
  Monitor). Force Quit encara mostra Python perquè requereix CFBundleName via
  .app bundle real (deute v0.9.1).
- Bug #9 — menu polish: server-nexe.com duplicat substituït per
  '📖 Documentació' al main level (3 idiomes), website_item es manté al
  submenú Configuració.

TRACK C — UX cosmètic
- Bug #5 — slow_request middleware exclou /ui/upload (uploads naturalment
  triguen >1s i el log apareixia duplicat amb l'access log d'uvicorn).
- Bug #8 — 3 ⓘ visibles als checkboxes del sidebar de col·leccions amb
  tooltips als 3 idiomes (la infraestructura CSS/i18n ja existia).

Pytest D-1 final: 4424 passed, 0 failed, 35 skipped, 1 xfailed, 86% coverage
en 76.11s. Baseline pre-BUS era 4396. +28 tests nous, ZERO regressions.

Tests nous:
- tests/test_pid_file.py: 7 tests Bug #1
- tests/test_ingest_knowledge_idempotent.py: 8 tests F7 (3 classes)
- plugins/web_ui_module/tests/test_memory_helper_async.py: 1 test F1
- plugins/web_ui_module/tests/test_memory_delete.py: 7 tests F3+Bug#10
- plugins/web_ui_module/tests/test_mem_save_injection.py: 5 tests Bug #3

Out of scope (deute v0.9.1+):
- routes_chat.py 54KB decapitació general (deute formal P0)
- Bundle .app real amb py2app per CFBundleName (deute v0.9.1)
- Resums per capítol (Part 2 redisseny memory)
- RDBMS font de veritat + vector store reconstruïble (HOMAD memoria v1, Part 2)
jgoy-labs added a commit that referenced this pull request Apr 21, 2026
…rence

Symptom: after a restart the sidebar only listed .json sessions (.enc
were invisible), new sessions were persisted unencrypted even with
encryption enabled, and a reboot's .json->.enc migration could
overwrite an existing .enc belonging to a different conversation with
the same id (collision observed in the wild: "Hola Diana" overwritten
by "Hola Anna").

Root cause is a three-bug chain:

1. Loader early-init (core/loader/manifest_base._get_module):
   calls `instance._init_router()` immediately after `__init__`,
   *before* `initialize()` runs. The plugin must have its routable
   resources ready at construction time.

2. Plugin double-create (plugins/web_ui_module/module.py):
   `__init__` created a SessionManager() without crypto, then
   `initialize()` replaced self.session_manager with a new
   SessionManager(crypto_provider=crypto). Two instances, divergent
   state: #1 loaded only .json, #2 loaded .enc + migrated .json->.enc.

3. Router local reference (plugins/web_ui_module/api/routes.py):
   `create_router()` captured session_mgr = module_instance.session_manager
   into a local at creation time. Because of bug #1, that snapshot was
   the crypto-less #1. When `initialize()` later replaced the attribute,
   the router closures kept pointing at the stale #1.

Net effect: the UI saw #1 (only .json sessions), new sessions went
through #1 and were written unencrypted, and every reboot migrated
those .json files into .enc, overwriting whatever .enc already lived
under the same id.

Fix:

- module.py: remove the premature SessionManager() from __init__; build
  the one real SessionManager(crypto_provider=crypto) in initialize()
  using whatever crypto_provider is available (may be None). One
  instance, one truth, one load-from-disk.

- routes.py: introduce _SessionManagerProxy. Instead of capturing
  session_mgr = module_instance.session_manager, route closures hold
  the proxy and `__getattr__` re-reads module_instance.session_manager
  on every request. Late-binding, so the real SessionManager built in
  initialize() is always what the routes hit. Raises a clear RuntimeError
  if a route fires before initialize() has completed (rather than the
  opaque 'NoneType has no attribute list_sessions').

Verified: after restart, a single "Loaded N sessions from disk" per
reboot (was 2 before), all .enc sessions visible in the sidebar, new
sessions persisted as .enc.

Data note: no encrypted sessions were lost in the wild. All existing
.enc files decrypt cleanly with the current MEK; they were only made
invisible by the stale reference. The only silent loss was the
conversation overwritten by the colliding-id migration, which predates
this fix.
jgoy-labs added a commit that referenced this pull request May 14, 2026
…clause SQL for nosec coverage

Onada 1 bandit audit — findings #2-#23 (22/23). Each remaining
HIGH+MEDIUM bandit finding from MUTHUR baseline 701b4e5 was reviewed
empirically by reading the actual call site, then either silenced as
a false positive (19) or marked ACCEPT with explicit justification (3).

Format: `# nosec B<rule>: <reason>` for FP, `# nosec B<rule>: ACCEPT — <reason>`
for accepted residual risk (so bandit silences the finding while the
ACCEPT prefix flags it as a conscious architectural decision rather
than a tooling false positive).

By rule:

  B104 ×2 (FP): host string comparison for allow-list construction in
    TrustedHostMiddleware / web_ui URL rewriting — not a network bind.
    Files: core/middleware.py, plugins/web_ui_module/module.py

  B108 ×2 FP + 1 ACCEPT: /tmp paths inside read-only allow-lists or
    detection arrays don't create files. The single ACCEPT is
    rag_logger.py last-resort fallback when both ~/Nexe-Logs and
    storage/logs are unwritable; mono-user local install threat model
    accepts the predictable-path race vs. silently disabling RAG
    telemetry.
    Files: installer/install.py, memory/memory/cli/rag_viewer.py,
    memory/memory/rag_logger.py

  B310 ×8 (FP): all are urlopen() against literal hardcoded
    http://localhost:{constant}/... URLs (Ollama daemon ps/tags,
    own server health, dev smoke-test). One additional case
    (core/cli/client.py) already validates the scheme against
    ALLOWED_URL_SCHEMES before urlopen.
    Files: core/cli/client.py, dev-tools/smoke_test.py (×2),
    installer/install.py, installer/tray.py (×2),
    installer/tray_monitor.py, plugins/web_ui_module/api/routes_auth.py

  B314 ×1 (FP): CI tooling parsing coverage.xml emitted by pytest-cov
    in the same workflow run.
    File: .github/scripts/generate_coverage_badge.py

  B608 ×6 (FP): the canonical IN-clause-with-dynamic-placeholders
    pattern, where placeholders = ",".join("?" for _ in n) and every
    value is bound as a parameter. The sqlite_store case additionally
    interpolates a table name validated by _validate_table() against
    a frozenset whitelist.
    Files: memory/memory/api/text_store.py,
    memory/memory/engines/persistence_sqlite.py,
    memory/memory/storage/sqlite_store.py,
    memory/memory/workers/{dreaming_cycle,gc_daemon,sync_worker}.py

    Note: 5 of the 6 SQL strings were also refactored from multi-line
    f"""...""" to single-line `sql = f"..."` + `conn.execute(sql, ...)`
    so the # nosec annotation lands on the line bandit reports
    (bandit doesn't follow # nosec across multi-line statements when
    the report points at an inner line). Functionally identical.

  B615 ×2 (ACCEPT): snapshot_download() without revision= pinning in
    `nexe pull` CLI path. Pinning infrastructure already exists at
    installer/installer_catalog_data.py:360 (MODEL_WEIGHT_SHA256 map,
    backlog item C19) but all entries are still None pending hash
    population. Realignment of CLI pull path scheduled for v1.0.5.
    File: core/cli/cli.py (×2)

Verified empirically:
  - 0 HIGH+MEDIUM bandit findings remain (was 23)
  - Total bandit count 195 → 172 (-23 exact)
  - Suite: 5127 passed, 0 regressions
  - 22 new # nosec annotations in production (1 preexisting B110 in
    plugins/mlx_module/core/chat.py:617 unrelated to this audit)

No behavioural changes.
jgoy-labs added a commit that referenced this pull request May 14, 2026
…nada 4.1 Cluster 12)

13 cosmetic mypy findings closed via documented type-ignores or minimal
annotations, no behavioural change:

import-untyped (toml, yaml — D3 director category)
- core/config.py:17 — toml lacks stubs; kept for write path (#3)
- core/cli/config.py:21 — yaml stubs deferred to v1.1 (#8)
- installer/install_headless.py:447 — toml lacks stubs (#15)
- installer/install.py:378 — toml lacks stubs (#16)
- core/cli/cli.py:325 — toml lacks stubs (#63)

no-redef (tomli fallback for Python <3.11)
- core/version.py:9 — # type: ignore[no-redef] (#11)
- core/cli/router.py:25 — # type: ignore[no-redef] (#12)

annotation/var-annotated/has-type
- core/cli/i18n.py:43 — FP or-narrowing of Optional[str] or str
  (Director Onada 4.1 D2: # type: ignore[return-value]) (#2)
- core/bootstrap_tokens.py:35 — declare _db_path / _initialized at
  class body and add -> 'BootstrapTokenManager' to __new__ (#6, #7)
- core/paths/detection.py:44 — _detection_history: list[dict[str, Any]]
  (#10)
- core/ingest/ingest_knowledge.py:243 — files: list[Path] = [] (#32)

method-assign
- scripts/bench_ingest_bug16.py:102 — benchmark monkey-patch documented
  with # type: ignore[method-assign] (#39)
jgoy-labs added a commit that referenced this pull request May 14, 2026
main() had 90 NLOC (over MUTHUR 80 limit) due to orgànic creixement
during the v0.9 release work. Extract 4 single-responsibility helpers
preserving exact initialization order:

- _set_process_title()        — setproctitle call (Bug #2)
- _setup_file_logging()       — TimedRotatingFileHandler setup
- _log_quick_commands_banner(host, port) — CLI banner
- _run_uvicorn_server(...)    — uvicorn.run + KeyboardInterrupt/Exception handling

main() now reads as a linear sequence of well-named steps.
40/40 runner-related tests pass (server, sidecar_port_guard, pid_file, tray_helpers).
jgoy-labs added a commit that referenced this pull request May 16, 2026
Fix-All BUS sobre 3 tracks paral·lels per resoldre tots els bugs del QA
post-BUS de normalització abans del DMG v0.9.0. 8 commits dev consolidats
en aquest sync.

TRACK A — Memory/RAG/Sessions
- Bug #1 (PID file canònic) — single source of truth a storage/run/server.pid
- F5 — 3 col·leccions canòniques (nexe_web_ui, user_knowledge, nexe_documentation)
  creades a get_memory_api() en lloc de només la primera
- F7 — ingest_knowledge defaulteja a nexe_documentation (era user_knowledge)
  i és idempotent (eliminada la sequence delete_collection + create_collection
  destructiva que esborrava docs ad-hoc dels usuaris a cada install)
- F8 — root cause empíric Bug #4: MemoryModule obria un SEGON QdrantClient
  real a storage/vectors/qdrant_local/, divergent del singleton del pool.
  MEM_SAVE escrivia a una col·lecció, MEM_RECALL llegia d'una altra.
  Ara tots dos comparteixen storage/vectors/.
- F1 — _check_duplicate retorna contracte honest (success=False, duplicate=True)
  enlloc de fingir success=True amb document_id=None. Era el segon root cause
  de Bug #4: el dedup bloquejava SAVEs amb fals positius silenciats.
- F2 — typo cols list (nexe_web_ui duplicat)
- F3 — list_memories scroll-based (sense semantic search amb query anglesa)
- Bug #10 — collections= filter a list/save/delete (sidebar checks reals)
- Bug #6 — frontend hydration document attached. Eren 2 bugs encadenats:
  l'endpoint /history no retornava attached_document, i removeFilePreview()
  feia POST /clear-document destructiu cada switch de sessió.
- Bug #3 — MEM_SAVE-only response fallback. Quan el model emet només
  [MEM_SAVE: ...] sense text envoltant, ara genera 'Memòria desada: <fact>'
  perquè el bloc save s'executi i el frontend mostri confirmació.
- auto_save crida eliminada per HOMAD memoria v1 (2026-04-01) — manual
  MEM_SAVE only fins a Part 2.

TRACK B — Tray / Multi-instance / Packaging
- Bug #1 (PID file) compartit amb Track A
- Bug #2 — setproctitle a server i tray (server-nexe / nexe-tray a ps/Activity
  Monitor). Force Quit encara mostra Python perquè requereix CFBundleName via
  .app bundle real (deute v0.9.1).
- Bug #9 — menu polish: server-nexe.com duplicat substituït per
  '📖 Documentació' al main level (3 idiomes), website_item es manté al
  submenú Configuració.

TRACK C — UX cosmètic
- Bug #5 — slow_request middleware exclou /ui/upload (uploads naturalment
  triguen >1s i el log apareixia duplicat amb l'access log d'uvicorn).
- Bug #8 — 3 ⓘ visibles als checkboxes del sidebar de col·leccions amb
  tooltips als 3 idiomes (la infraestructura CSS/i18n ja existia).

Pytest D-1 final: 4424 passed, 0 failed, 35 skipped, 1 xfailed, 86% coverage
en 76.11s. Baseline pre-BUS era 4396. +28 tests nous, ZERO regressions.

Tests nous:
- tests/test_pid_file.py: 7 tests Bug #1
- tests/test_ingest_knowledge_idempotent.py: 8 tests F7 (3 classes)
- plugins/web_ui_module/tests/test_memory_helper_async.py: 1 test F1
- plugins/web_ui_module/tests/test_memory_delete.py: 7 tests F3+Bug#10
- plugins/web_ui_module/tests/test_mem_save_injection.py: 5 tests Bug #3

Out of scope (deute v0.9.1+):
- routes_chat.py 54KB decapitació general (deute formal P0)
- Bundle .app real amb py2app per CFBundleName (deute v0.9.1)
- Resums per capítol (Part 2 redisseny memory)
- RDBMS font de veritat + vector store reconstruïble (HOMAD memoria v1, Part 2)
jgoy-labs added a commit that referenced this pull request May 16, 2026
…rence

Symptom: after a restart the sidebar only listed .json sessions (.enc
were invisible), new sessions were persisted unencrypted even with
encryption enabled, and a reboot's .json->.enc migration could
overwrite an existing .enc belonging to a different conversation with
the same id (collision observed in the wild: "Hola Diana" overwritten
by "Hola Anna").

Root cause is a three-bug chain:

1. Loader early-init (core/loader/manifest_base._get_module):
   calls `instance._init_router()` immediately after `__init__`,
   *before* `initialize()` runs. The plugin must have its routable
   resources ready at construction time.

2. Plugin double-create (plugins/web_ui_module/module.py):
   `__init__` created a SessionManager() without crypto, then
   `initialize()` replaced self.session_manager with a new
   SessionManager(crypto_provider=crypto). Two instances, divergent
   state: #1 loaded only .json, #2 loaded .enc + migrated .json->.enc.

3. Router local reference (plugins/web_ui_module/api/routes.py):
   `create_router()` captured session_mgr = module_instance.session_manager
   into a local at creation time. Because of bug #1, that snapshot was
   the crypto-less #1. When `initialize()` later replaced the attribute,
   the router closures kept pointing at the stale #1.

Net effect: the UI saw #1 (only .json sessions), new sessions went
through #1 and were written unencrypted, and every reboot migrated
those .json files into .enc, overwriting whatever .enc already lived
under the same id.

Fix:

- module.py: remove the premature SessionManager() from __init__; build
  the one real SessionManager(crypto_provider=crypto) in initialize()
  using whatever crypto_provider is available (may be None). One
  instance, one truth, one load-from-disk.

- routes.py: introduce _SessionManagerProxy. Instead of capturing
  session_mgr = module_instance.session_manager, route closures hold
  the proxy and `__getattr__` re-reads module_instance.session_manager
  on every request. Late-binding, so the real SessionManager built in
  initialize() is always what the routes hit. Raises a clear RuntimeError
  if a route fires before initialize() has completed (rather than the
  opaque 'NoneType has no attribute list_sessions').

Verified: after restart, a single "Loaded N sessions from disk" per
reboot (was 2 before), all .enc sessions visible in the sidebar, new
sessions persisted as .enc.

Data note: no encrypted sessions were lost in the wild. All existing
.enc files decrypt cleanly with the current MEK; they were only made
invisible by the stale reference. The only silent loss was the
conversation overwritten by the colliding-id migration, which predates
this fix.
jgoy-labs added a commit that referenced this pull request May 16, 2026
…clause SQL for nosec coverage

Onada 1 bandit audit — findings #2-#23 (22/23). Each remaining
HIGH+MEDIUM bandit finding from quality checks baseline 701b4e5 was reviewed
empirically by reading the actual call site, then either silenced as
a false positive (19) or marked ACCEPT with explicit justification (3).

Format: `# nosec B<rule>: <reason>` for FP, `# nosec B<rule>: ACCEPT — <reason>`
for accepted residual risk (so bandit silences the finding while the
ACCEPT prefix flags it as a conscious architectural decision rather
than a tooling false positive).

By rule:

  B104 ×2 (FP): host string comparison for allow-list construction in
    TrustedHostMiddleware / web_ui URL rewriting — not a network bind.
    Files: core/middleware.py, plugins/web_ui_module/module.py

  B108 ×2 FP + 1 ACCEPT: /tmp paths inside read-only allow-lists or
    detection arrays don't create files. The single ACCEPT is
    rag_logger.py last-resort fallback when both ~/Nexe-Logs and
    storage/logs are unwritable; mono-user local install threat model
    accepts the predictable-path race vs. silently disabling RAG
    telemetry.
    Files: installer/install.py, memory/memory/cli/rag_viewer.py,
    memory/memory/rag_logger.py

  B310 ×8 (FP): all are urlopen() against literal hardcoded
    http://localhost:{constant}/... URLs (Ollama daemon ps/tags,
    own server health, dev smoke-test). One additional case
    (core/cli/client.py) already validates the scheme against
    ALLOWED_URL_SCHEMES before urlopen.
    Files: core/cli/client.py, dev-tools/smoke_test.py (×2),
    installer/install.py, installer/tray.py (×2),
    installer/tray_monitor.py, plugins/web_ui_module/api/routes_auth.py

  B314 ×1 (FP): CI tooling parsing coverage.xml emitted by pytest-cov
    in the same workflow run.
    File: .github/scripts/generate_coverage_badge.py

  B608 ×6 (FP): the canonical IN-clause-with-dynamic-placeholders
    pattern, where placeholders = ",".join("?" for _ in n) and every
    value is bound as a parameter. The sqlite_store case additionally
    interpolates a table name validated by _validate_table() against
    a frozenset whitelist.
    Files: memory/memory/api/text_store.py,
    memory/memory/engines/persistence_sqlite.py,
    memory/memory/storage/sqlite_store.py,
    memory/memory/workers/{dreaming_cycle,gc_daemon,sync_worker}.py

    Note: 5 of the 6 SQL strings were also refactored from multi-line
    f"""...""" to single-line `sql = f"..."` + `conn.execute(sql, ...)`
    so the # nosec annotation lands on the line bandit reports
    (bandit doesn't follow # nosec across multi-line statements when
    the report points at an inner line). Functionally identical.

  B615 ×2 (ACCEPT): snapshot_download() without revision= pinning in
    `nexe pull` CLI path. Pinning infrastructure already exists at
    installer/installer_catalog_data.py:360 (MODEL_WEIGHT_SHA256 map,
    backlog item C19) but all entries are still None pending hash
    population. Realignment of CLI pull path scheduled for v1.0.5.
    File: core/cli/cli.py (×2)

Verified empirically:
  - 0 HIGH+MEDIUM bandit findings remain (was 23)
  - Total bandit count 195 → 172 (-23 exact)
  - Suite: 5127 passed, 0 regressions
  - 22 new # nosec annotations in production (1 preexisting B110 in
    plugins/mlx_module/core/chat.py:617 unrelated to this audit)

No behavioural changes.
jgoy-labs added a commit that referenced this pull request May 16, 2026
…nada 4.1 Cluster 12)

13 cosmetic mypy findings closed via documented type-ignores or minimal
annotations, no behavioural change:

import-untyped (toml, yaml — D3 director category)
- core/config.py:17 — toml lacks stubs; kept for write path (#3)
- core/cli/config.py:21 — yaml stubs deferred to v1.1 (#8)
- installer/install_headless.py:447 — toml lacks stubs (#15)
- installer/install.py:378 — toml lacks stubs (#16)
- core/cli/cli.py:325 — toml lacks stubs (#63)

no-redef (tomli fallback for Python <3.11)
- core/version.py:9 — # type: ignore[no-redef] (#11)
- core/cli/router.py:25 — # type: ignore[no-redef] (#12)

annotation/var-annotated/has-type
- core/cli/i18n.py:43 — FP or-narrowing of Optional[str] or str
  (Director Onada 4.1 D2: # type: ignore[return-value]) (#2)
- core/bootstrap_tokens.py:35 — declare _db_path / _initialized at
  class body and add -> 'BootstrapTokenManager' to __new__ (#6, #7)
- core/paths/detection.py:44 — _detection_history: list[dict[str, Any]]
  (#10)
- core/ingest/ingest_knowledge.py:243 — files: list[Path] = [] (#32)

method-assign
- scripts/bench_ingest_bug16.py:102 — benchmark monkey-patch documented
  with # type: ignore[method-assign] (#39)
jgoy-labs added a commit that referenced this pull request May 16, 2026
main() had 90 NLOC (over quality checks 80 limit) due to orgànic creixement
during the v0.9 release work. Extract 4 single-responsibility helpers
preserving exact initialization order:

- _set_process_title()        — setproctitle call (Bug #2)
- _setup_file_logging()       — TimedRotatingFileHandler setup
- _log_quick_commands_banner(host, port) — CLI banner
- _run_uvicorn_server(...)    — uvicorn.run + KeyboardInterrupt/Exception handling

main() now reads as a linear sequence of well-named steps.
40/40 runner-related tests pass (server, sidecar_port_guard, pid_file, tray_helpers).
jgoy-labs added a commit that referenced this pull request May 16, 2026
…rence

Symptom: after a restart the sidebar only listed .json sessions (.enc
were invisible), new sessions were persisted unencrypted even with
encryption enabled, and a reboot's .json->.enc migration could
overwrite an existing .enc belonging to a different conversation with
the same id (collision observed in the wild: "Hola Diana" overwritten
by "Hola Anna").

Root cause is a three-bug chain:

1. Loader early-init (core/loader/manifest_base._get_module):
   calls `instance._init_router()` immediately after `__init__`,
   *before* `initialize()` runs. The plugin must have its routable
   resources ready at construction time.

2. Plugin double-create (plugins/web_ui_module/module.py):
   `__init__` created a SessionManager() without crypto, then
   `initialize()` replaced self.session_manager with a new
   SessionManager(crypto_provider=crypto). Two instances, divergent
   state: #1 loaded only .json, #2 loaded .enc + migrated .json->.enc.

3. Router local reference (plugins/web_ui_module/api/routes.py):
   `create_router()` captured session_mgr = module_instance.session_manager
   into a local at creation time. Because of bug #1, that snapshot was
   the crypto-less #1. When `initialize()` later replaced the attribute,
   the router closures kept pointing at the stale #1.

Net effect: the UI saw #1 (only .json sessions), new sessions went
through #1 and were written unencrypted, and every reboot migrated
those .json files into .enc, overwriting whatever .enc already lived
under the same id.

Fix:

- module.py: remove the premature SessionManager() from __init__; build
  the one real SessionManager(crypto_provider=crypto) in initialize()
  using whatever crypto_provider is available (may be None). One
  instance, one truth, one load-from-disk.

- routes.py: introduce _SessionManagerProxy. Instead of capturing
  session_mgr = module_instance.session_manager, route closures hold
  the proxy and `__getattr__` re-reads module_instance.session_manager
  on every request. Late-binding, so the real SessionManager built in
  initialize() is always what the routes hit. Raises a clear RuntimeError
  if a route fires before initialize() has completed (rather than the
  opaque 'NoneType has no attribute list_sessions').

Verified: after restart, a single "Loaded N sessions from disk" per
reboot (was 2 before), all .enc sessions visible in the sidebar, new
sessions persisted as .enc.

Data note: no encrypted sessions were lost in the wild. All existing
.enc files decrypt cleanly with the current MEK; they were only made
invisible by the stale reference. The only silent loss was the
conversation overwritten by the colliding-id migration, which predates
this fix.
jgoy-labs added a commit that referenced this pull request May 16, 2026
…clause SQL for nosec coverage

Onada 1 bandit audit — findings #2-#23 (22/23). Each remaining
HIGH+MEDIUM bandit finding from quality checks baseline 701b4e5 was reviewed
empirically by reading the actual call site, then either silenced as
a false positive (19) or marked ACCEPT with explicit justification (3).

Format: `# nosec B<rule>: <reason>` for FP, `# nosec B<rule>: ACCEPT — <reason>`
for accepted residual risk (so bandit silences the finding while the
ACCEPT prefix flags it as a conscious architectural decision rather
than a tooling false positive).

By rule:

  B104 ×2 (FP): host string comparison for allow-list construction in
    TrustedHostMiddleware / web_ui URL rewriting — not a network bind.
    Files: core/middleware.py, plugins/web_ui_module/module.py

  B108 ×2 FP + 1 ACCEPT: /tmp paths inside read-only allow-lists or
    detection arrays don't create files. The single ACCEPT is
    rag_logger.py last-resort fallback when both ~/Nexe-Logs and
    storage/logs are unwritable; mono-user local install threat model
    accepts the predictable-path race vs. silently disabling RAG
    telemetry.
    Files: installer/install.py, memory/memory/cli/rag_viewer.py,
    memory/memory/rag_logger.py

  B310 ×8 (FP): all are urlopen() against literal hardcoded
    http://localhost:{constant}/... URLs (Ollama daemon ps/tags,
    own server health, dev smoke-test). One additional case
    (core/cli/client.py) already validates the scheme against
    ALLOWED_URL_SCHEMES before urlopen.
    Files: core/cli/client.py, dev-tools/smoke_test.py (×2),
    installer/install.py, installer/tray.py (×2),
    installer/tray_monitor.py, plugins/web_ui_module/api/routes_auth.py

  B314 ×1 (FP): CI tooling parsing coverage.xml emitted by pytest-cov
    in the same workflow run.
    File: .github/scripts/generate_coverage_badge.py

  B608 ×6 (FP): the canonical IN-clause-with-dynamic-placeholders
    pattern, where placeholders = ",".join("?" for _ in n) and every
    value is bound as a parameter. The sqlite_store case additionally
    interpolates a table name validated by _validate_table() against
    a frozenset whitelist.
    Files: memory/memory/api/text_store.py,
    memory/memory/engines/persistence_sqlite.py,
    memory/memory/storage/sqlite_store.py,
    memory/memory/workers/{dreaming_cycle,gc_daemon,sync_worker}.py

    Note: 5 of the 6 SQL strings were also refactored from multi-line
    f"""...""" to single-line `sql = f"..."` + `conn.execute(sql, ...)`
    so the # nosec annotation lands on the line bandit reports
    (bandit doesn't follow # nosec across multi-line statements when
    the report points at an inner line). Functionally identical.

  B615 ×2 (ACCEPT): snapshot_download() without revision= pinning in
    `nexe pull` CLI path. Pinning infrastructure already exists at
    installer/installer_catalog_data.py:360 (MODEL_WEIGHT_SHA256 map,
    backlog item C19) but all entries are still None pending hash
    population. Realignment of CLI pull path scheduled for v1.0.5.
    File: core/cli/cli.py (×2)

Verified empirically:
  - 0 HIGH+MEDIUM bandit findings remain (was 23)
  - Total bandit count 195 → 172 (-23 exact)
  - Suite: 5127 passed, 0 regressions
  - 22 new # nosec annotations in production (1 preexisting B110 in
    plugins/mlx_module/core/chat.py:617 unrelated to this audit)

No behavioural changes.
jgoy-labs added a commit that referenced this pull request May 16, 2026
…nada 4.1 Cluster 12)

13 cosmetic mypy findings closed via documented type-ignores or minimal
annotations, no behavioural change:

import-untyped (toml, yaml — D3 director category)
- core/config.py:17 — toml lacks stubs; kept for write path (#3)
- core/cli/config.py:21 — yaml stubs deferred to v1.1 (#8)
- installer/install_headless.py:447 — toml lacks stubs (#15)
- installer/install.py:378 — toml lacks stubs (#16)
- core/cli/cli.py:325 — toml lacks stubs (#63)

no-redef (tomli fallback for Python <3.11)
- core/version.py:9 — # type: ignore[no-redef] (#11)
- core/cli/router.py:25 — # type: ignore[no-redef] (#12)

annotation/var-annotated/has-type
- core/cli/i18n.py:43 — FP or-narrowing of Optional[str] or str
  (Director Onada 4.1 D2: # type: ignore[return-value]) (#2)
- core/bootstrap_tokens.py:35 — declare _db_path / _initialized at
  class body and add -> 'BootstrapTokenManager' to __new__ (#6, #7)
- core/paths/detection.py:44 — _detection_history: list[dict[str, Any]]
  (#10)
- core/ingest/ingest_knowledge.py:243 — files: list[Path] = [] (#32)

method-assign
- scripts/bench_ingest_bug16.py:102 — benchmark monkey-patch documented
  with # type: ignore[method-assign] (#39)
jgoy-labs added a commit that referenced this pull request May 16, 2026
main() had 90 NLOC (over quality checks 80 limit) due to orgànic creixement
during the v0.9 release work. Extract 4 single-responsibility helpers
preserving exact initialization order:

- _set_process_title()        — setproctitle call (Bug #2)
- _setup_file_logging()       — TimedRotatingFileHandler setup
- _log_quick_commands_banner(host, port) — CLI banner
- _run_uvicorn_server(...)    — uvicorn.run + KeyboardInterrupt/Exception handling

main() now reads as a linear sequence of well-named steps.
40/40 runner-related tests pass (server, sidecar_port_guard, pid_file, tray_helpers).
jgoy-labs added a commit that referenced this pull request May 26, 2026
Synced from dev since
sync-20260519. Highlights:

Security (Tier S — security audit 96/100 PUSH OK):
- S2 XSS api_key UI sanitization (web_ui_module/ui/app.js)
- S3 /installer/finalize legacy gate via idempotency marker
- S9 path traversal reject in _resolve_model_path
- S10 SQLiteStore thread-safe via check_same_thread + RLock
- S1 health URL boot fix
- S6 fsync before close on atomic writes
- S7 CancelledError catch + mid-startup signal logging
- TOCTOU atomic on /installer/finalize idempotency marker
- basename guard applied to all model_id download pipelines

Bug fixes vespre (2026-05-21 sessions):
- Bug #5 fix(qdrant): retry-with-backoff lock acquisition at startup
- Bug #4 test(live): order slow LLM tests after fast ones + 10s cooldown
- Bug #2+#3 test(security): replace fragile inspect-based assertions
- Bug #1 chore(installer): document accepted CVEs + portable OSV checker
- Bug A fix(chat): remove data:[DONE] sentinel from text/plain stream
- Bug B iter-2 fix(chat): natural-language date phrase replaces "Now:" tech

UI productiva (F5.5 revert + post-revert):
- web_ui_module serves full UI in sidecar mode again
- footer i18n + version persistence
- frontend reads nexe_api_key from query string on first launch

F5.3 + F5.4 onboarding wizard:
- HTTP endpoints for wizard (installer/finalize, preflight, progress)
- NEXE_VERSION UI injection
- onboarding_state.py + optional HF token via macOS Keychain (never on disk)
- installer_constants.py + installer_progress.py + check_cves_osv.py

Hygiene:
- 113 hardcoded "Nexe 0.9" → version centralized (UI)
- /v1/system/* blocklist on sidecar URL guard
- ruff/mypy/pyright/typos cleanup (multiple commits)

54 files changed total. Nous tests: 17 (security regression sentinels +
onboarding wizard + qdrant + installer + sqlite concurrent + sqlite store
singleton).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant