llm-dev-core 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/ARCHITECTURE.md +7 -7
  2. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/CHANGELOG.md +20 -0
  3. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/PKG-INFO +1 -1
  4. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/README.md +3 -3
  5. llm_dev_core-0.4.0/docs/decisions/0003-secure-base-contract.md +66 -0
  6. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/metrics/README.md +1 -1
  7. llm_dev_core-0.4.0/packages/secure-base/pyproject.toml +15 -0
  8. llm_dev_core-0.4.0/packages/secure-base/src/secure_base/__init__.py +14 -0
  9. llm_dev_core-0.4.0/packages/secure-base/src/secure_base/detectors.py +52 -0
  10. llm_dev_core-0.4.0/packages/secure-base/src/secure_base/findings.py +9 -0
  11. llm_dev_core-0.4.0/packages/secure-base/src/secure_base/profile.py +27 -0
  12. llm_dev_core-0.4.0/packages/secure-base/src/secure_base/redact.py +36 -0
  13. llm_dev_core-0.4.0/packages/secure-base/tests/test_secure_base.py +74 -0
  14. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/pyproject.toml +2 -1
  15. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.github/workflows/ci.yml +0 -0
  16. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.github/workflows/publish.yml +0 -0
  17. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.gitignore +0 -0
  18. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.python-version +0 -0
  19. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/Makefile +0 -0
  20. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/decisions/0000-stack.md +0 -0
  21. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/decisions/0001-llm-client-contract.md +0 -0
  22. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/decisions/0002-schema-validate-contract.md +0 -0
  23. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/runbooks/README.md +0 -0
  24. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/pyproject.toml +0 -0
  25. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/scripts/record_tape_opencode.py +0 -0
  26. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/__init__.py +0 -0
  27. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/cache.py +0 -0
  28. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/client.py +0 -0
  29. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/models.py +0 -0
  30. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/pricing.py +0 -0
  31. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/provider.py +0 -0
  32. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/providers/__init__.py +0 -0
  33. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/providers/openai_compat.py +0 -0
  34. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/providers/opencode_cli.py +0 -0
  35. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/replay.py +0 -0
  36. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/span.py +0 -0
  37. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/__init__.py +0 -0
  38. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/cassettes/complete-opencode--big-pickle-2.jsonl +0 -0
  39. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/conftest.py +0 -0
  40. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_openai_compat.py +0 -0
  41. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_pricing_cache.py +0 -0
  42. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_real_tape.py +0 -0
  43. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_replay.py +0 -0
  44. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_span.py +0 -0
  45. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/pyproject.toml +0 -0
  46. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/__init__.py +0 -0
  47. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/registry.py +0 -0
  48. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/validate.py +0 -0
  49. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/tests/test_schema_validate.py +0 -0
  50. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/scripts/check_consumers.py +0 -0
  51. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/scripts/collect_metrics.py +0 -0
  52. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/scripts/compat_check.sh +0 -0
  53. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/consumer-readme.md +0 -0
  54. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/core-consumer.yml +0 -0
  55. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/eval-dataset.jsonl +0 -0
  56. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/prompt.md +0 -0
  57. {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/retrofit-issue.md +0 -0
@@ -1,6 +1,6 @@
1
1
  # ARCHITECTURE.md — llm-dev-core
2
2
 
3
- > **Estado:** v0.3 (seed revisado) · **Última revisión:** 2026-09-21
3
+ > **Estado:** v0.4 (seed revisado) · **Última revisión:** 2026-09-22
4
4
  > **Audiencia:** yo en 52 semanas, cualquiera que revise este repo, y entrevistadores técnicos.
5
5
 
6
6
  ---
@@ -131,7 +131,7 @@ La reparación tiene **presupuesto**: máximo 2 intentos y un cap de costo por m
131
131
 
132
132
  ```yaml
133
133
  id: commit-message-generator
134
- version: 0.3.0
134
+ version: 0.4.0
135
135
  owner: llm-dev-core
136
136
  model_family: gpt-4o-mini
137
137
  schema: CommitMessage
@@ -339,11 +339,11 @@ La regla es simple: si no puedes pagar el eval de forma repetible, el eval está
339
339
 
340
340
  ## 10. Estado actual
341
341
 
342
- - **Versión:** v0.3
343
- - **Módulos existentes:** `packages/llm-client` v0.1.0 (seed sem. 2) — `complete`/`stream`, retry+backoff+jitter, reparación con tope, cap de costo, cache, span de 20 campos, record/replay nativo, transportes `opencode` (sesión local sin API key) y `openai-compatible` (HTTP). `packages/schema-validate` v0.1.0 (seed sem. 3) — validación Pydantic de la salida LLM (ADR-2): JSON con/sin caretas, errores campo a campo, registro `SchemaId → modelo`, `parsed` en `ValidationResult`; compone con `llm-client` vía el hook `validator`.
344
- - **Consumidores:** `Proyectos/commit-cli` (sem. 2) — CLI que propone mensajes de commit desde `git diff`; `Proyectos/release-scribe` (sem. 3) — release notes JSON validadas por `schema-validate` desde `git log`. Ambos dependen de la dist unificada `llm-dev-core` (editable). La métrica de reutilización real se medirá en §10.1 desde el corte de semanal.
345
- - **Empaquetado:** una sola dist `llm-dev-core`, publicable vía `uv build`/`uv publish` con `llm_client` y `schema_validate` top-level (D1). Pendiente la primera publicación real en PyPI (se cierra en la sem. 3 con v0.3).
346
- - **Enforcement:** ADR-0, ADR-1 (`llm-client-contract`) y ADR-2 (`schema-validate-contract`) en `docs/decisions/`, plantillas en `templates/`, `make validate` (estructura + tests) y `make validate-consumer CONSUMER=../<repo>`.
342
+ - **Versión:** v0.4
343
+ - **Módulos existentes:** `packages/llm-client` v0.1.0 (seed sem. 2) — `complete`/`stream`, retry+backoff+jitter, reparación con tope, cap de costo, cache, span de 20 campos, record/replay nativo, transportes `opencode` (sesión local sin API key) y `openai-compatible` (HTTP). `packages/schema-validate` v0.1.0 (seed sem. 3) — validación Pydantic de la salida LLM (ADR-2): JSON con/sin caretas, errores campo a campo, registro `SchemaId → modelo`, `parsed` en `ValidationResult`; compone con `llm-client` vía el hook `validator`. `packages/secure-base` v0.1.0 (seed sem. 4) — utilidad de máscara (ADR-3): `detect`/`redact`/`sanitize_for_prompt`/`assert_redacted` + `SecurityProfile`; máscara unidireccional y determinista (placeholder `[REDACTED:<tipo>:<n>]`), 9 detectores, hoja de la seguridad (no depende de `llm-client` ni `schema-validate`); compone en los renderers, antes de que el texto cruce al proveedor.
344
+ - **Consumidores:** `Proyectos/commit-cli` (sem. 2) — CLI que propone mensajes de commit desde `git diff`; `Proyectos/release-scribe` (sem. 3) — release notes JSON validadas por `schema-validate` desde `git log`; `Proyectos/sec-check` (sem. 4) — escáner de secretos con triaje LLM: redacta con `secure-base` y aborta si `assert_redacted` no es vacío, reporte `sec-findings-v1` validado por `schema-validate`. Los tres dependen de la dist unificada `llm-dev-core` (editable). La métrica de reutilización real se medirá en §10.1 desde el corte de semanal.
345
+ - **Empaquetado:** una sola dist `llm-dev-core` (D1) publicada en PyPI: `llm_client`, `schema_validate` y `secure_base` top-level. La publicación es automática desde tag `v*` vía trusted publishing OIDC (workflow `publish.yml`, sin tokens guardados).
346
+ - **Enforcement:** ADR-0, ADR-1 (`llm-client-contract`), ADR-2 (`schema-validate-contract`) y ADR-3 (`secure-base-contract`) en `docs/decisions/`, plantillas en `templates/`, `make validate` (estructura + tests) y `make validate-consumer CONSUMER=../<repo>`.
347
347
  - **Pasos por semana según el plan:** se mantiene el calendario semanal (estructura C1–C18 de §5, armonizada en la sem. 3): `schema-validate` (sem. 3), `secure-base` (sem. 4), `test-kit` (sem. 5), `web-api-base` (sem. 6), `ci-pack` (sem. 7), `cache-ratelimit` (sem. 8), `bot-base` (sem. 9), `parser-io` (sem. 10), `docs-gen` (sem. 12), ... hasta `cost-obs` (sem. 39).
348
348
  - **Próximo hito:** v1.0 en la semana 13, con 12 consumidores reales detrás.
349
349
 
@@ -2,6 +2,24 @@
2
2
 
3
3
  La versión es la del paquete `llm-dev-core`. El historial comienza con **v1.0 (semana 13)** — hasta entonces el core es `0.x` y puede romperse sin aviso (D3).
4
4
 
5
+ ## v0.4 — 2026-09-22 (secure-base seed)
6
+
7
+ - Semana 4: nace `packages/secure-base` v0.1.0 dentro de un consumidor real (`sec-check`).
8
+ - Contrato ADR-3: `detect`/`redact`/`sanitize_for_prompt`/`assert_redacted` + `SecurityProfile` (declaración
9
+ de tipos para el lint anti-secretos de `ci-pack`, sem. 7).
10
+ - Máscara unidireccional y determinista: placeholder `[REDACTED:<tipo>:<n>]`; `Finding.match` es preview
11
+ truncado (24 chars), nunca el secreto completo. 9 detectores (email, phone, ipv4, credit_card, aws_access_key,
12
+ github_token, private_key, bearer, url_userinfo).
13
+ - `secure-base` es hoja (no depende de `llm-client` ni de `schema-validate`); se compone en el renderer del
14
+ consumidor, antes de que el texto cruce al proveedor.
15
+ - Tercer consumidor: `Proyectos/sec-check` (sem. 4) — escáner de secretos con triaje LLM: detecta en `git diff`
16
+ o archivo, redacta con `secure-base`, aborta si `assert_redacted` no es vacío, y solo el texto enmascarado viaja
17
+ al LLM (`opencode` local o replay). Reporte `sec-findings-v1` validado con `schema-validate`; exit 0 sin secretos,
18
+ 1 con secretos, 2 inválido. Tape real grabado con 3 hallazgos críticos, replay en verde.
19
+ - Wheel unificado (D1): `llm-dev-core 0.4.0` ahora empaqueta `llm_client` + `schema_validate` + `secure_base`
20
+ top-level. `make validate` y `validate-consumer` (3 consumidores) en verde; 37 tests.
21
+ - Publicado `llm-dev-core 0.4.0` en PyPI (trusted publishing, tag `v0.4.0`).
22
+
5
23
  ## v0.3 — 2026-09-21 (schema-validate seed + dist unificada)
6
24
 
7
25
  - Semana 3: nace `packages/schema-validate` v0.1.0 dentro de un consumidor real (`release-scribe`).
@@ -19,6 +37,8 @@ La versión es la del paquete `llm-dev-core`. El historial comienza con **v1.0 (
19
37
  `web-api-base` (6), `ci-pack` (7), `cache-ratelimit` (8), `bot-base` (9), … hasta `cost-obs` (39).
20
38
  Deuda aprobada en el review: la sem-2 quería publicar ya en PyPI; al unificar el empaquetado, la primera
21
39
  publicación real pasa a ser esta v0.3.
40
+ - Publicado `llm-dev-core 0.3.0` en PyPI (trusted publishing OIDC, workflow `publish.yml` en tags `v*`):
41
+ primera release real del core; instalable y verificado desde el registro (D1).
22
42
 
23
43
  ## v0.2 — 2026-09-20 (llm-client seed)
24
44
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: llm-dev-core
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Núcleo versionado del que dependen 52 proyectos en 52 semanas: un solo sistema acumulativo.
5
5
  Author-email: binahco <binahco.sas@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -6,15 +6,15 @@ Núcleo versionado y publicado del que dependen 52 proyectos en 52 semanas: un s
6
6
 
7
7
  Toda llamada a un LLM pasa por `llm-client`, toda salida no confiable pasa por `schema-validate`, todo prompt vive en `prompt-registry`. Ver `ARCHITECTURE.md` para la tesis, las decisiones (D1–D12) y las métricas verificables.
8
8
 
9
- - **Estado:** v0.3 · Semana 3
9
+ - **Estado:** v0.4 · Semana 4
10
10
  - **Stack:** Python 3.12+ (ADR-0, `docs/decisions/0000-stack.md`)
11
11
  - **Estructura:** `packages/` (módulos), `templates/` (consumidor clonable), `scripts/` (verificación), `docs/`
12
- - **Empaquetado:** una sola dist `llm-dev-core` (D1): `llm_client` y `schema_validate` top-level; los consumidores dependen de `llm-dev-core ^0.x`, nunca de módulos sueltos.
12
+ - **Empaquetado:** una sola dist `llm-dev-core` (D1): `llm_client`, `schema_validate` y `secure_base` top-level; los consumidores dependen de `llm-dev-core ^0.x`, nunca de módulos sueltos.
13
13
 
14
14
  ## Desarrollo
15
15
 
16
16
  ```bash
17
17
  uv sync # instala el workspace
18
18
  make validate # invariantes: estructura, plantillas, ADR-0, sintaxis
19
- uv run pytest # tests (aún ninguno hasta sem. 2)
19
+ uv run pytest # tests (37 a la fecha)
20
20
  ```
@@ -0,0 +1,66 @@
1
+ # Contrato mínimo — `secure-base`
2
+
3
+ - **Fecha:** 2026-09-22
4
+ - **Estado:** aceptada
5
+ - **Origen:** §3.1 en `ARCHITECTURE.md`
6
+ - **Implementada en:** semana 4 (`packages/secure-base` v0.1.0, nacida dentro de `sec-check`)
7
+
8
+ Este documento es el contrato escrito contra el que se implementa la semana 4. Define la utilidad de
9
+ máscara que exige §3.1.1 y el perfil de declaración que alimentará los lints de `ci-pack` (sem. 7).
10
+
11
+ ## 1. API conceptual
12
+
13
+ ```python
14
+ detect(text) -> list[Finding] # hallazgos tipados (sin mutar texto)
15
+ redact(text) -> RedactResult # texto enmascarado + hallazgos
16
+ sanitize_for_prompt(text) -> RedactResult # alias semántico para renderers
17
+ assert_redacted(text, profile?) -> list[Finding] # violaciones de §3.1
18
+ ```
19
+
20
+ ```python
21
+ class Finding(BaseModel):
22
+ type: str # email | phone | ipv4 | credit_card | aws_access_key | github_token | private_key | bearer | url_userinfo
23
+ match: str # preview truncado (24 chars) — NUNCA el secreto completo
24
+ offset: int # offset del primer carácter
25
+ length: int # para reemplazos deterministas
26
+ line: int # 1-based
27
+ ```
28
+
29
+ ## 2. Reglas de la máscara
30
+
31
+ - **Unidireccional:** el secreto crudo no se conserva en ningún lugar del resultado. No hay operación inversa.
32
+ - **Determinista:** misma entrada → misma salida (mismo texto y mismos placeholders).
33
+ - **Placeholder:** `[REDACTED:<type>:<n>]`, con `n` por tipo (1-based, en orden de aparición).
34
+ - **`Finding.match` es un preview truncado**: suficiente para triaje, insuficiente para re-inyectar el secreto.
35
+ - **`detect` es no destructivo**: el scanner se usa antes de decidir qué se envía; `redact` produce la salida
36
+ que sí puede cruzar el límite del prompt.
37
+
38
+ ## 3. Perfil y lints (§3.1.1 → ci-pack)
39
+
40
+ - `SecurityProfile(data_types: list[str])`: qué tipos declara manejar el consumidor.
41
+ - `assert_redacted(text, profile) -> list[Finding]`: si el resultado **no está vacío**, el texto aún contiene
42
+ secretos en claro declarados por el perfil → violación de la regla 1 de §3.1.
43
+ - El enforcement operativo (que el build falle si un prompt de producción llega con secretos) se implementa
44
+ en `ci-pack` (sem. 7); `secure-base` es el motor que ese lint usaría.
45
+
46
+ ## 4. Composición con el resto del core
47
+
48
+ - `secure-base` **no depende de `llm-client`** ni de `schema-validate`: es la hoja de la seguridad. Se compone
49
+ en el renderer del consumidor (antes de construir los mensajes) y en el preámbulo del flujo.
50
+ - `llm-client` redacta `<variable>` en sus spans por contrato (ADR-1, §1); `secure-base` hace lo mismo aguas
51
+ arriba, sobre el payload que podría entrar a un prompt.
52
+
53
+ ## 5. Qué NO contrata este documento
54
+
55
+ - No define el lint anti-prompts (sem. 7, `ci-pack`): solo su motor de detección.
56
+ - No reemplaza el manejo de secretos de la máquina del desarrollador: no es un gestor de claves.
57
+ - No garantiza detección perfecta: los patrones son heurísticos; la prueba de que algo no se filtró es
58
+ estructural (el texto que sale de `redact` no contiene ningún hallazgo).
59
+
60
+ ## 6. Criterio de aceptación del contrato (semana 4)
61
+
62
+ - [x] `detect` encuentra los 9 tipos en un texto de prueba y no delira con código limpio.
63
+ - [x] `redact` es unidireccional y determinista: nada del secreto crudo sobrevive; mismo input → mismo output.
64
+ - [x] `sanitize_for_prompt` deja `assert_redacted(...) == []` (el texto que cruza al proveedor está limpio).
65
+ - [x] `assert_redacted(text, profile)` filtra por los tipos declarados.
66
+ - [x] Primer consumidor real: `sec-check` (sem. 4), que redacta diffs antes de enviarlos al LLM.
@@ -2,6 +2,6 @@
2
2
 
3
3
  Datos crudos y agregados de la página de evidencia (§10.1).
4
4
 
5
- Estado actual: sin datos. Semanas 1–3 tienen consumidores (`commit-cli`, `release-scribe`) pero aún no hay
5
+ Estado actual: sin datos. Semanas 1–4 tienen consumidores (`commit-cli`, `release-scribe`, `sec-check`) pero aún no hay
6
6
  llamadas agregadas ni corriendo en CI. El andamiaje (`scripts/collect_metrics.py`) se implementa en la
7
7
  semana 6 con `ci-pack`; a partir de ahí `docs/metrics/` se regenera en cada merge.
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "llm-dev-secure-base"
3
+ version = "0.1.0"
4
+ description = "secure-base: secretos y datos sensibles nunca entran en prompts sin redacción (§3.1)."
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "pydantic>=2.7",
8
+ ]
9
+
10
+ [build-system]
11
+ requires = ["hatchling"]
12
+ build-backend = "hatchling.build"
13
+
14
+ [tool.hatch.build.targets.wheel]
15
+ packages = ["src/secure_base"]
@@ -0,0 +1,14 @@
1
+ from .findings import Finding
2
+ from .profile import DATA_TYPE_NAMES, SecurityProfile, assert_redacted
3
+ from .redact import RedactResult, detect, redact, sanitize_for_prompt
4
+
5
+ __all__ = [
6
+ "DATA_TYPE_NAMES",
7
+ "Finding",
8
+ "RedactResult",
9
+ "SecurityProfile",
10
+ "assert_redacted",
11
+ "detect",
12
+ "redact",
13
+ "sanitize_for_prompt",
14
+ ]
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+ from .findings import Finding
7
+
8
+
9
+ def _preview(raw: str, limit: int = 24) -> str:
10
+ match = raw[:limit]
11
+ return match + "…" if len(raw) > limit else match
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Detector:
16
+ type: str
17
+ pattern: re.Pattern[str]
18
+
19
+ def scan(self, text: str) -> list[Finding]:
20
+ findings: list[Finding] = []
21
+ for m in self.pattern.finditer(text):
22
+ raw = m.group(0)
23
+ findings.append(
24
+ Finding(
25
+ type=self.type,
26
+ match=_preview(raw),
27
+ offset=m.start(),
28
+ length=len(raw),
29
+ line=text.count("\n", 0, m.start()) + 1,
30
+ )
31
+ )
32
+ return findings
33
+
34
+
35
+ DETECTORS: tuple[Detector, ...] = (
36
+ Detector("email", re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")),
37
+ Detector("phone", re.compile(r"(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b")),
38
+ Detector("ipv4", re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")),
39
+ Detector("credit_card", re.compile(r"\b(?:\d{4}[ -]?){3}\d{4}\b")),
40
+ Detector("aws_access_key", re.compile(r"\b(?:AKIA|ASIA)[A-Z0-9]{16}\b")),
41
+ Detector("github_token", re.compile(r"\b(?:ghp_|gho_|ghu_|ghs_|ghr_|github_pat_)[A-Za-z0-9_]{20,}\b")),
42
+ Detector("private_key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----")),
43
+ Detector("bearer", re.compile(r"\bBearer [A-Za-z0-9._~+/-]+=*\b")),
44
+ Detector("url_userinfo", re.compile(r"(?i)\b\w+://[\w.-]+:[^@\s]+@")),
45
+ )
46
+
47
+
48
+ def scan(text: str) -> list[Finding]:
49
+ findings: list[Finding] = []
50
+ for detector in DETECTORS:
51
+ findings.extend(detector.scan(text))
52
+ return sorted(findings, key=lambda f: (f.offset, f.type))
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Finding(BaseModel):
5
+ type: str
6
+ match: str
7
+ offset: int
8
+ length: int
9
+ line: int
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from .detectors import scan
6
+ from .findings import Finding
7
+
8
+ DATA_TYPE_NAMES = ("email", "phone", "ipv4", "credit_card", "aws_access_key", "github_token", "private_key", "bearer", "url_userinfo")
9
+
10
+
11
+ class SecurityProfile(BaseModel):
12
+ """Qué tipos de datos declara manejar el consumidor (§3.1 y futuro lint de ci-pack)."""
13
+
14
+ data_types: list[str]
15
+
16
+
17
+ def assert_redacted(text: str, profile: SecurityProfile | None = None) -> list[Finding]:
18
+ """Devuelve hallazgos que siguen en claro en `text`.
19
+
20
+ Si el texto se va a enviar a un proveedor externo, un resultado no vacío es una violación.
21
+ Con `profile`, filtra a los tipos declarados por el consumidor.
22
+ """
23
+ findings = scan(text)
24
+ if profile is None:
25
+ return findings
26
+ declared = set(profile.data_types)
27
+ return [f for f in findings if f.type in declared]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from .detectors import scan
6
+ from .findings import Finding
7
+
8
+
9
+ class RedactResult(BaseModel):
10
+ text: str
11
+ findings: list[Finding]
12
+
13
+
14
+ def detect(text: str) -> list[Finding]:
15
+ """Detecta secretos/PII sin redactar. Alimenta reportes y lints."""
16
+ return scan(text)
17
+
18
+
19
+ def redact(text: str) -> RedactResult:
20
+ """Enmascara de forma unidireccional y determinista: el secreto crudo no queda en el texto."""
21
+ findings = scan(text)
22
+ counters: dict[str, int] = {}
23
+ pieces: list[str] = []
24
+ cursor = 0
25
+ for f in findings:
26
+ pieces.append(text[cursor : f.offset])
27
+ counters[f.type] = counters.get(f.type, 0) + 1
28
+ pieces.append(f"[REDACTED:{f.type}:{counters[f.type]}]")
29
+ cursor = f.offset + f.length
30
+ pieces.append(text[cursor:])
31
+ return RedactResult(text="".join(pieces), findings=findings)
32
+
33
+
34
+ def sanitize_for_prompt(text: str) -> RedactResult:
35
+ """Alias semántico para el renderer: nada sensible entra al prompt."""
36
+ return redact(text)
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from secure_base import SecurityProfile, assert_redacted, detect, redact, sanitize_for_prompt
4
+
5
+ SAMPLE = """prueba con persona@ejemplo.com y también conta@otra.co
6
+ token ghp_AbCdEfGhIjKlMnOpQrStUvWxYz123456
7
+ aws AKIAIOSFODNN7EXAMPLE en plena línea
8
+ privada:
9
+ -----BEGIN RSA PRIVATE KEY-----
10
+ incluye https://usuario:pass@host.com/path
11
+ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc
12
+ tel +57 300 123 4567 y tarjeta 4111 1111 1111 1111
13
+ ip 192.168.0.1 cliente"""
14
+
15
+
16
+ def test_detect_types_positive() -> None:
17
+ findings = detect(SAMPLE)
18
+ types = {f.type for f in findings}
19
+ assert {
20
+ "email",
21
+ "github_token",
22
+ "aws_access_key",
23
+ "private_key",
24
+ "url_userinfo",
25
+ "bearer",
26
+ "phone",
27
+ "credit_card",
28
+ "ipv4",
29
+ } == types
30
+
31
+
32
+ def test_no_false_positive_on_plain_text() -> None:
33
+ findings = detect("solo código normal sin secretos: return None")
34
+ assert findings == []
35
+
36
+
37
+ def test_redact_is_one_way_and_deterministic() -> None:
38
+ result = redact(SAMPLE)
39
+ assert "persona@ejemplo.com" not in result.text
40
+ assert "ghp_" not in result.text
41
+ assert "AKIA" not in result.text
42
+ assert "usuario:pass" not in result.text
43
+ assert "Bearer eyJ" not in result.text
44
+ second = redact(SAMPLE)
45
+ assert result.text == second.text
46
+
47
+
48
+ def test_redact_keeps_placeholders_and_structure() -> None:
49
+ result = redact(SAMPLE)
50
+ assert "[REDACTED:email:1]" in result.text
51
+ assert "[REDACTED:private_key:1]" in result.text
52
+ assert result.findings
53
+ assert result.findings[0].line >= 1
54
+
55
+
56
+ def test_sanitize_for_prompt_never_leaves_raw_secrets() -> None:
57
+ clean = sanitize_for_prompt(SAMPLE)
58
+ assert assert_redacted(clean.text) == []
59
+
60
+
61
+ def test_assert_redacted_filters_by_profile() -> None:
62
+ profile = SecurityProfile(data_types=["email"])
63
+ findings = assert_redacted(SAMPLE, profile)
64
+ assert findings and all(f.type == "email" for f in findings)
65
+
66
+ ok = assert_redacted(redact(SAMPLE).text, profile)
67
+ assert ok == []
68
+
69
+
70
+ def test_detect_reports_offset_and_line() -> None:
71
+ findings = detect(SAMPLE)
72
+ email = next(f for f in findings if f.type == "email")
73
+ assert email.offset == SAMPLE.find("persona@ejemplo.com")
74
+ assert SAMPLE.count("\n", 0, email.offset) + 1 == 1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llm-dev-core"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Núcleo versionado del que dependen 52 proyectos en 52 semanas: un solo sistema acumulativo."
5
5
  authors = [{ name = "binahco", email = "binahco.sas@gmail.com" }]
6
6
  requires-python = ">=3.12"
@@ -18,6 +18,7 @@ build-backend = "hatchling.build"
18
18
  packages = [
19
19
  "packages/llm-client/src/llm_client",
20
20
  "packages/schema-validate/src/schema_validate",
21
+ "packages/secure-base/src/secure_base",
21
22
  ]
22
23
 
23
24
  [tool.uv.workspace]
File without changes
File without changes