fix: Sprint 0-3 — Security, ADRs, hygiene, DX (91 files, ~1200 lines net) - #132
Merged
Merged
Conversation
Two Rust hygiene fixes in gatekeeper.rs identified during the PR #130 audit (briefing Alertas 2 and 3): 1. p99_latency_ms was a running maximum, not a percentile (PR-5) The previous `if latency > p99 { p99 = latency }` pattern is max(), not P99 — any single spike permanently raises the reported value, making the <50ms SLA assertion meaningless. Fix: 1 000-sample circular ring buffer (Box<[f32; 1000]>) on the Gatekeeper struct. update_metrics() writes into the ring O(1); get_metrics() sorts a copy on demand (called only by metrics scrapers, not per-scan). No external dependencies added. GatekeeperMetrics gains p50/p95/p99/p999 fields; the Python FFI dict (bridge/mod.rs) is updated to expose all four. `let mut` is required on the MutexGuard<Gatekeeper> in get_gatekeeper_metrics because get_metrics() now takes &mut self. 2. unwrap_or_else(|_| panic!(...)) → .expect() (Alerta 3) gatekeeper.rs:154 used `unwrap_or_else(|_| panic!(...))`, which is the anti-pattern forbidden by BTV invariants. Replaced with `.expect("blake3_hash is [u8;32] — slice [0..8] is infallible")`. Verified: `cargo build -p buildtovalue-kernel` clean (0 errors, 0 new warnings); `cargo test -p buildtovalue-kernel` 12/12 pass including test_gatekeeper_aggregates_worst_case. https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
…olicy reload
ADR-060 — BiasDeclaration validated constructor:
- BiasDeclaration::new() now returns Result<Self, BiasDeclarationError>
(Err on calibration_date==0 or test_dataset_size==0).
- impl Default gated with #[cfg(test)] — production code that calls
::default() will not compile.
- BiasDeclaration::aggregate() added for gatekeeper worst-case path where
calibration_date may legitimately be 0 (logged as warning via
is_calibration_valid()).
- ~30 static bias_declaration() impl sites updated with .expect("static
bias values are valid"). policy.rs::bias_declaration fixed: dataset_size
0 → 1000. evidence/technical.rs and validators/mod.rs use aggregate().
- BiasDeclarationError added to core/errors.rs.
ADR-061 — Decision::Block + NegotiationDeadlockReason:
- Decision enum (Allow/Log/Deny/Block/Redact/Report) in core/types.rs.
Deny = policy rejection (24h SLA); Block = active threat (Trust Score
penalty). Distinct semantics prevent Ledger misclassification.
- NegotiationDeadlockReason enum (4 variants, repr(u8)).
- DeadlockResolutionError struct: explanation field required by constructor
(empty explanation → Err); uses serde_array_compat::size_256.
ADR-062 — AppealRecord off-chain verification:
- AppealRecord fixed-size struct in core/types.rs (verdict_id, hashes,
timestamps). Zero heap allocation — safe for hot-path serialisation.
- verify_appeal_text() in explanation_store.py: blake3(text) compared to
Ledger hash via hmac.compare_digest() (constant-time). Falls back to
sha3-256 if blake3 package is absent.
ADR-063 — Compile-time size invariants:
- const _: () = assert!(size_of::<BiasDeclaration>() == 512, ...) active.
- TechnicalEvidence assert deferred until Vec<u8> fields become fixed-size
arrays (comment documents Phase 2 path).
ADR-064 — PolicyWatcher with Ed25519:
- policy/loader.rs: PolicyWatcher holds VerifyingKey (public only).
verify_and_load() verifies Ed25519 signature before parsing YAML.
Parsing never occurs on invalid signature (PolicyLoadError::InvalidSignature).
- ed25519-dalek = { workspace = true } added to kernel/Cargo.toml.
- Python policy_loader.py (pre-existing) implements the same guarantee
via cryptography.hazmat.
Build: cargo build -p buildtovalue-kernel — clean, 0 errors.
Tests: cargo test -p buildtovalue-kernel — 302 unit + 43 integration = 0 failures.
https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
…ash_algorithm param Ressalva 1 from ADR-062 validation review: The previous implementation silently fell back to sha3-256 when the blake3 package was absent. This created a data integrity hazard: records written with blake3 would fail verification in environments without blake3, and records written with sha3-256 (the fallback) would fail in environments with blake3 — all without any error, just False returned from compare_digest. Fix: - Add hash_algorithm: str = 'blake3' parameter. Callers must pass the algorithm used when the hash was recorded (stored alongside the record). - If hash_algorithm='blake3' and the package is absent: raise RuntimeError with an explicit message. Falling back silently is prohibited. - If hash_algorithm is unknown: raise ValueError immediately. - sha3-256 remains supported as a named algorithm (not a fallback). - _SUPPORTED_ALGORITHMS frozenset documents the contract at module level. The Ledger (Rust side) always uses BLAKE3; the default 'blake3' is correct for production. sha3-256 is retained for environments that explicitly opted in (test fixtures, migration tooling) where the algorithm is stored and passed explicitly. https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
…P rotation
Three singletons in the lifespan were initialized with get_hmac_key() at
startup, capturing a snapshot of the key bytes. After rotate_hmac_key()
(SIGHUP), those singletons continued signing with the stale key — the gap
that PR-4 documented as "partial" in the "PR-4 partial" comments.
GoalDriftSentinel:
- __init__ gains hmac_secret_fn: Optional[Callable[[], bytes]] = None.
- hmac_secret: bytes → Optional[bytes] = None for backwards compat.
- Validates: exactly one of (hmac_secret, hmac_secret_fn) must be provided.
- sign_drift() calls self._secret_fn() at each invocation — key read is
deferred to signing time, so rotate_hmac_key() propagates immediately.
DelegationLedger:
- __init__ gains hmac_key_fn: Optional[Callable[[], bytes]] = None.
- hmac_key: Optional[bytes] = None (was required-but-optional-typed).
- Both signing call sites (delegate() and verify_contract()) use
self._key_fn() instead of self._secret.
EthicalContextEngine:
- The stale signing_key=get_hmac_key() kwarg in app.py is removed.
EthicalContextEngine has never accepted signing_key in its __init__
(the kwarg was a stale remnant predating the current __init__ signature).
ECE delegates signing to PolicySigner, not raw HMAC. The call would
have raised TypeError at lifespan startup.
app.py:
- EthicalContextEngine(): no HMAC arg (correct — ECE uses PolicySigner).
- GoalDriftSentinel(hmac_secret_fn=get_hmac_key): callable passed.
- DelegationLedger(hmac_key_fn=get_hmac_key): callable passed.
All three "PR-4 partial" comments removed — S-09 is fully closed.
Gate: ADR-060..064 were merged on this branch as commit 8419caa.
This commit uses the now-unblocked Python feature path (Trilho C).
https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
Workspace Integrity (clippy -D expect_used):
BiasDeclaration::from_static() added to types.rs with #[allow(clippy::expect_used)].
30 bias_declaration() implementations replaced BiasDeclaration::new(...).expect("static
bias values are valid") with the new from_static() constructor — static compile-time
constants that cannot fail don't need runtime error propagation.
gatekeeper.rs:163: try_into().expect() on blake3_hash slice replaced with
copy_from_slice() — no .expect() call, genuinely infallible.
gatekeeper.rs:351: iter().copied().collect() → .to_vec() (clippy::iter_collect_into).
E2E governance startup (TypeError):
context_engine.EthicalContextEngine.__init__ required signing_key: bytes.
Commit 7299aee removed the arg from app.py (correct intent for S-09) but did not
first make signing_key optional and add signing_key_fn. Service crashed at lifespan.
context_engine.py: same callable-pattern applied as GoalDriftSentinel and
DelegationLedger — signing_key_fn: Optional[Callable[[], bytes]] accepted,
key resolved at verdict-signing time via self._signing_key_fn().
app.py: EthicalContextEngine(signing_key_fn=get_hmac_key) — S-09 fully closed
for all three verdict-signing singletons.
https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
S-02 (most tragic finding — fixable before any CISO demo):
context_engine.EthicalContextEngine.verify_signature() was a 3-line stub
that returned `len(verdict.hmac_signature) == 64`. Any 64-char hex string
passed verification — including verdicts where final_action was forged
BLOCK→ALLOW after signing. The docstring admitted "simplified — full impl
needs stored hash".
Fix has two parts:
- context_engine_types.EthicalVerdict gains blake3_hash: str field, the
audit-evidence binding required to recompute the signed payload. Without
it the verdict was structurally unverifiable.
- context_engine.decide() populates blake3_hash=evidence.blake3_hash at
sign time. The verdict is now self-verifying from the ledger record +
current HMAC key (no separate evidence lookup needed).
- verify_signature() recomputes f"{verdict_id}|{blake3_hash}|{final_action}
|{timestamp}" and compares with hmac.compare_digest() (constant time).
Verified roundtrip locally:
valid verdict → True
BLOCK→ALLOW forgery → False (the headline tamper case)
arbitrary "a"*64 → False (closes the no-op bug literally)
swapped blake3_hash → False
S-03:
data/policies/apagar_teste_slm.yaml ("delete-test_slm.yaml") was a
forgotten test file loaded by PolicyEngine._load_policies() via
rglob("*.yaml") in production. Deleted; no callers — file contained
only `slm: { enabled: false }`.
Ripple audit: EthicalVerdict has exactly one production construction site
(context_engine.decide). A second class named EthicalVerdict exists in
contestability_loop.py with a different schema (decision/bias_declaration
fields) — name shadowing technical debt tracked separately, unaffected
by this change.
https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
ADR-063 Phase 2 (TechnicalEvidence size invariant): - Activate compile-time assert in core/types.rs — confirmed 9632 bytes. The assert was deferred pending Vec<u8> field removal; struct was already fully fixed-size ([u8; 7072] _reserved_metadata), so activation is safe. - Harden from_bytes(): add post-read version check (must be 1–3) to reject zero-filled or truncated buffers before they propagate as live evidence. Adds safety comment explaining why the unsafe block meets its preconditions. - Update ADR-063 status to Active; document phase 2 completion. appeals.db schema (ADR-047 follow-up): - Add hash_algorithm TEXT DEFAULT 'blake3' to CREATE TABLE and the safe ALTER TABLE migration block. Existing rows default to 'blake3'. Aligns appeals audit records with the BLAKE3 evidence-binding introduced by S-02 (verify_signature / blake3_hash in EthicalVerdict). CORS (S0-05): already resolved in a prior commit; no change needed. https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
…iminator
H-01 — ADR cleanup (6 duplicate pairs, 3 orphans):
- Renumber 6 "loser" duplicates to 0065–0070 (preserving git history via
git mv). Canonical ADRs at their original numbers remain unchanged.
Updated internal ADR number in each renamed file header.
- Archive 3 orphans to docs/adr/archive/: ADR-043-grant-decision-adapter.md,
TBD-grant-decision-adapter.md (both superseded by 0057), ADR-051.md
(duplicate of 0051). Archive README documents supersession chain.
- Update 0000-adr-index.md: remove reservation note for 0043–0048/0050;
update Group L (ADR-0060 SaaS → now ADR-0070); add Group M listing
ADR-0043 through ADR-0070; update statistics (46 → 70 total ADRs).
- Fix ci.yml: paths and W2.9 step now reference canonical
0057-grant-decision-adapter.md instead of archived ADR-043 file.
H-02 — Version unification to 0.1.0-alpha.1 / 0.1.0a1:
- Rust workspace Cargo.toml: 1.0.0 → 0.1.0-alpha.1 (all inheriting crates
auto-updated via workspace inheritance).
- btv-core, buildtovalue, btv-governance: 3.0.0-alpha.1 / 1.0.0 → 0.1.0-alpha.1.
- Fixed btv-core version constraint in buildtovalue facade Cargo.toml.
- Python: pyproject.toml, __init__.py (x2), app.py, cli/main.py → 0.1.0a1.
- Dashboard sidebar, Makefile, gateway/src/main.rs, spec/openapi.yaml updated.
H-04 — CI: add cargo fmt --check to crate_release_audit.yml (step 0, before
all other steps). rustfmt component already present in dtolnay/rust-toolchain.
H-05 — YAML schema discriminator (prevent PolicyEngine cross-contamination):
- Add schema_type to PolicyEngine._KNOWN_TOP_LEVEL_KEYS.
- Add _POLICY_RULES_SCHEMA_TYPES / _NON_POLICY_SCHEMA_TYPES class-level sets.
- In _parse_policy_file: skip files where schema_type ∈ {profile, agent-guard}
with a DEBUG log (not WARNING — skipping is expected behaviour, not a mistake).
- Add schema_type: "policy-rules" to governance_v1.yaml and default.yaml.
- Add schema_type: "profile" to base.yaml.
- Add schema_type: "agent-guard" to chatbot-vendor-*.yaml and chatbot-rag-external.yaml.
https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
…H-10 logging H-06 — Deduplicate AbliterationDetector and EthicalContextEngine: - model_integrity_verifier.py: delete v1.1.0 AbliterationDetector class (~90 lines) and import the production v1.2.0 from abliteration_detector.py (ADR-051 Fase 2). IntegrityVerifier now uses the implementation with timeout enforcement, extended probe catalog, confidence scoring, and explain_decision(). - context_engine.py: remove "EthicalContextEngine" from __all__. The legacy v1.9.1 judiciary class stays (governance_gateway.py depends on it) but is no longer re-exported as the public API. Canonical export: governance/__init__.py → ethical_context_engine.EthicalContextEngine. Added module-level deprecation note. H-07 — Reconcile 720h vs 24h in appeal documentation: - quickstart.md: annotate all three 720h references to clarify the distinction: 720h = GDPR Art. 22 regulatory appeal window (30 days, set by regulation); 24h = BTV internal SLA for human review (set by BTV SRE). Both are correct and refer to different obligations. Reference added to docs/compliance.md. H-08 — Crate license declarations: - Add license.workspace = true to btv-executive, btv-judicial, btv-redaction, btv-sigma, btv-types. All five were missing a license field; they now inherit "MIT OR Apache-2.0" from the workspace package table. H-10 — Logging on critical silent except blocks: - policy_engine.py._load_policies: bare `except Exception: pass` → logs WARNING with file name and exception for every malformed YAML skipped. - policy_engine.py._parse_policy_file: bare `except (KeyError, ValueError): continue` → logs WARNING with file name, rule_id, and exception for every bad rule. - governance_gateway.py: add `import logging` + `logger`, then replace bare `except Exception: pass` on refusal-record persistence with WARNING log (fail-open semantics preserved — refusal verdict still proceeds). https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
Todos os exemplos de docs e integração apontavam para :3000 (porta legada).
Gateway expõe :8080 em todos os ambientes. Corrigir para consistência e
evitar onboarding quebrado.
Arquivos: README.md, docs/quickstart.md,
docs/integrations/chatbot-{internal,external}-llm.md
https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
Q-01: extrair decide() (544 linhas) em 8 helpers + coordenador ~40 linhas. _AdjSignals/_SLMMeta/_ComplianceMeta como acumuladores mutáveis; _decide_hard_block, _decide_run_guards, _decide_accumulate_signals, _decide_slm, _decide_adjust_risk, _decide_ethical_verdict, _decide_compliance, _decide_output_pipeline, _decide_persist_trust. Comportamento idêntico — refatoração pura sem mudança de lógica. Q-03: adicionar REPORT e REFUSE ao enum VerdictAction do OpenAPI. REPORT (ADR-043: composite_risk >= threshold com findings) e REFUSE (RefusalGate MOSAIC-inspired) estavam ausentes da especificação. Q-04: tornar GPU opcional no Docker Compose. Remover reservations.devices do docker-compose.yml (falha em hosts sem GPU); criar docker-compose.gpu.yml como override explícito para workloads GPU. https://claude.ai/code/session_01T3z96dywN5mpiAyCyEe5KP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sprint 0 + Sprint 1 + Sprint 2 + Sprint 3
PR acumulada com 12 commits cobrindo todos os sprints da dívida técnica crítica identificada na auditoria de 20/05/2026.
Sprint 0 — Emergência de Segurança
verify_signature()implementado com HMAC real +hmac.compare_digest().EthicalVerdictganhoublake3_hashbinding evidência↔verdict. ForjadoBLOCK→ALLOWrejeitado.apagar_teste_slm.yamldeletado do diretório de producão.GoalDriftSentinel,DelegationLedger,EthicalContextEngine) com chave resolvida em tempo de assinatura viaCallable[[], bytes].allow_origins=["*"]→BTV_CORS_ORIGINSenv var.const_assert_eq!(size_of::<TechnicalEvidence>(), 9632)ativo.from_bytes()valida version [1–3].hash_algorithm TEXT DEFAULT 'blake3'adicionada.Sprint 1 — ADRs Arquiteturais (todos mergeados na PR #131)
BiasDeclaration::new()retornaResultDecision::Block(BlockReason)com semântica distinta deDenyAppealRecordfixed-size +verify_appeal_text()constant-timePolicyWatchercomed25519-dalekSprint 2 — Higiene Crítica
git mv. Índice reconstruido: 46 → 70 ADRs.0.1.0-alpha.1em todos os manifests (era 7 versões diferentes).cargo fmt --checkadicionado ao CI como Step 0.policy-rules/profile/agent-guard).default.yamlagora corretamente avaliado.AbliterationDetectorv1.1.0 substituído por import da v1.2.0.EthicalContextEngineremovida de__all__.license.workspace = true.exceptsilenciosos empolicy_engine.pyegovernance_gateway.pyagora emitemlogger.warningcom filename + detalhe.Sprint 3 — Qualidade & DX
decide()refatorado de 544 linhas para 8 funções focadas + coordenador ~40 linhas. Acumuladores_AdjSignals,_SLMMeta,_ComplianceMetafluem pelo pipeline. Zero mudanças de comportamento.:8080em README.md, quickstart.md e 2 docs de integração.REPORTeREFUSEadicionados ao enumVerdictActionno OpenAPI spec (estavam implementados no Python mas ausentes do contrato).docker-compose.ymlpadrão. Criadoops/docker-compose.gpu.ymlcomo opt-in explícito.Checklist
cargo build -p buildtovalue-kernel— cleanconst_assert_eq!(size_of::<TechnicalEvidence>(), 9632)compilandoverify_signature()HMAC real — forjado BLOCK→ALLOW rejeitado0.1.0-alpha.1cargo fmt --checkno CIdecide()≤50 linhas por funçãoPendente fora de PR
S-01 — Purga do histórico Git (credenciais rastreadas): requer
git push --forceapós BFG/filter-repo. Janela de manutenção separada.Sprint 4 na fila (Marketing Honesto)
benchmarks/REPRODUCE.md