Security minor release. Two P1 fixes addressing structural-correctness gaps surfaced by an internal security review. No public-API breaks — every new behaviour is opt-in. Existing deployments see no surprise.
FIX 1 — Fail-loud cryptographic configuration
Six places in the codebase used silent try / except: pass patterns around signer initialisation. The behaviour is now:
- Explicit config + broken = REFUSE. Operator misconfigured; raise
ConfigurationError, exit non-zero. - No config + safe default exists = WARN loudly. Continue in explicitly-marked unsigned mode.
- Never silent.
Affected paths:
raucle_detect/errors.py— new module:ConfigurationError,PolicyUnproven.raucle_detect.server._init_compliance()— extracted from import-time init;RAUCLE_DETECT_VERDICT_KEY_PEMorRAUCLE_DETECT_AUDIT_PRIVATE_KEY_PEMset but unparseable now refuses to start.VerdictSigner.__init__/Ed25519Signer.__init__— a signer whose public key cannot be extracted raisesConfigurationErrorat construction (was: silently produced unverifiable receipts).audit.sink_from_env()— invalidRAUCLE_DETECT_AUDIT_PRIVATE_KEY_PEMraises instead of silently producing an unsigned sink.HashChainSink— every newly-created chain emits a self-describingchain_metaheader on line 1, signed when a signer is supplied.AuditVerifiersurfacessigned_mode ∈ {signed, unsigned, unknown}and rejects splice attempts (signed checkpoint in unsigned chain, mismatchedkey_idbetween header and checkpoint).
FIX 2 — Strict proof-enforced mint mode
Previously, cap mint --policy-proof-hash was advisory: any string could be passed. Tokens cited proofs they had no structural relationship to. The new strict mode makes the relationship enforceable.
from raucle_detect.capability import CapabilityIssuer
from raucle_detect.prove import JSONSchemaProver
# 1. Prove the policy
result = JSONSchemaProver().prove(schema=tool_schema, policy=tool_policy)
assert result.status == "PROVEN"
# 2. Strict-mint binds proof + grammar + policy hashes into the token
issuer = CapabilityIssuer.generate(issuer="acme.bank.kyc", require_proof=True)
token = issuer.mint(
agent_id="agent:kyc-prod",
tool="lookup_customer",
proof_result=result, # required
)
# 3. The token now carries:
# token.policy_proof_hash == result.hash
# token.grammar_hash == result.grammar_hash
# token.policy_hash == result.policy_hash
# All three covered by token_id and the Ed25519 signature.Defence-in-depth at the gate too:
from raucle_detect.capability import CapabilityGate
gate = CapabilityGate(
trusted_issuers={...},
proof_enforcement_mode=\"strict\",
trusted_proofs={result.hash: result}, # pre-loaded cache
)
# Every call now denied unless:
# * token has policy_proof_hash, AND
# * proof in trusted_proofs cache, AND
# * proof.status == 'PROVEN', AND
# * token.grammar_hash matches proof.grammar_hash, AND
# * token.policy_hash matches proof.policy_hashCLI:
# Strict mint:
raucle-detect cap mint \\
--key issuer.key.pem --issuer acme.bank.kyc \\
--agent-id agent:kyc-prod --tool lookup_customer \\
--require-proof --proof-result proof.json \\
--out token.jsonraucle-detect prove json|url|sql already returned exit-code 2 on REFUTED and 1 on UNDECIDED — both pipeline-failing under set -e. Now covered by tests so CI never silently regresses.
Backward compatibility
| Default | Opt-in via | |
|---|---|---|
| Strict mint mode | OFF | CapabilityIssuer(require_proof=True) or RAUCLE_REQUIRE_PROOF=1 |
| Gate-time proof enforcement | OFF | `CapabilityGate(proof_enforcement_mode='lenient' |
Chain chain_meta header |
always emitted on new chains | n/a — additive on-wire format; legacy chains remain verifiable |
| Fail-loud crypto errors | always loud | n/a — replaces silent except-pass with ConfigurationError |
The two Capability field additions (grammar_hash, policy_hash) are nullable; absent in pre-0.13.0 tokens. Round-tripped through to_dict() / from_dict() cleanly.
Tests
70 new tests across tests/test_audit.py, tests/test_verdicts.py, tests/test_capability.py, tests/test_server_init.py, tests/test_cli_exit_codes.py. Full suite: 429 passed, 4 skipped. No regressions.
What's not in this release
- Hosted policy-registry fetcher (gate consults a published registry at runtime with TTL caching).
[DECIDE: gate-time proof enforcement]comment inCapabilityGate.__init__flags this as future scope; current gate-time enforcement uses a caller-supplied in-memory cache. - Argon2id password hashes server-side. Out of scope for this fix.
🤖 Generated with Claude Code