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.
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/ARCHITECTURE.md +7 -7
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/CHANGELOG.md +20 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/PKG-INFO +1 -1
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/README.md +3 -3
- llm_dev_core-0.4.0/docs/decisions/0003-secure-base-contract.md +66 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/metrics/README.md +1 -1
- llm_dev_core-0.4.0/packages/secure-base/pyproject.toml +15 -0
- llm_dev_core-0.4.0/packages/secure-base/src/secure_base/__init__.py +14 -0
- llm_dev_core-0.4.0/packages/secure-base/src/secure_base/detectors.py +52 -0
- llm_dev_core-0.4.0/packages/secure-base/src/secure_base/findings.py +9 -0
- llm_dev_core-0.4.0/packages/secure-base/src/secure_base/profile.py +27 -0
- llm_dev_core-0.4.0/packages/secure-base/src/secure_base/redact.py +36 -0
- llm_dev_core-0.4.0/packages/secure-base/tests/test_secure_base.py +74 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/pyproject.toml +2 -1
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.github/workflows/ci.yml +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.github/workflows/publish.yml +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.gitignore +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/.python-version +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/Makefile +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/decisions/0000-stack.md +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/decisions/0001-llm-client-contract.md +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/decisions/0002-schema-validate-contract.md +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/docs/runbooks/README.md +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/pyproject.toml +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/scripts/record_tape_opencode.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/__init__.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/cache.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/client.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/models.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/pricing.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/provider.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/providers/__init__.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/providers/openai_compat.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/providers/opencode_cli.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/replay.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/span.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/__init__.py +0 -0
- {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
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/conftest.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_openai_compat.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_pricing_cache.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_real_tape.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_replay.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/tests/test_span.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/pyproject.toml +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/__init__.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/registry.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/validate.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/tests/test_schema_validate.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/scripts/check_consumers.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/scripts/collect_metrics.py +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/scripts/compat_check.sh +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/consumer-readme.md +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/core-consumer.yml +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/eval-dataset.jsonl +0 -0
- {llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/templates/prompt.md +0 -0
- {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
|
+
> **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.
|
|
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.
|
|
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`.
|
|
345
|
-
- **Empaquetado:** una sola dist `llm-dev-core
|
|
346
|
-
- **Enforcement:** ADR-0, ADR-1 (`llm-client-contract`)
|
|
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
|
|
|
@@ -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.
|
|
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 `
|
|
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 (
|
|
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–
|
|
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,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
|
+
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/scripts/record_tape_opencode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/llm-client/src/llm_client/providers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/__init__.py
RENAMED
|
File without changes
|
{llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/registry.py
RENAMED
|
File without changes
|
{llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/src/schema_validate/validate.py
RENAMED
|
File without changes
|
{llm_dev_core-0.3.0 → llm_dev_core-0.4.0}/packages/schema-validate/tests/test_schema_validate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|