Skip to content

Commit 1e60f60

Browse files
fix(idris): rebuild logSafeBounded proof for Idris2 0.8.0 (SafeAPIKey baseline rot) (#116)
## Summary Restores `Boj.SafeAPIKey` to a green Idris2 0.8.0 build. The pre-existing `logSafeBounded` proof on `main` does not type-check; this PR rebuilds it against the current stdlib + elaborator behaviour. **No new axioms.** The 5-class-(J) framing established in #108 is preserved. Discovered while verifying #108 (item 11). Was originally stacked on #108; rebased onto `main` after #108 merged at 2026-05-20T08:13Z. ## Three independent defects, each fixed 1. **Removed the redundant local `plusLteMonotone` helper.** It wrapped `lteTransitive (plusLteMonotoneRight _ lmn) (plusLteMonotoneLeft _ lpq)`: - `lteTransitive` is no longer in stdlib — Idris2 0.8.0 uses `Control.Relation.transitive`, but that's ambiguous in this import context. - `plusLteMonotoneRight` / `plusLteMonotoneLeft` have signature `(p, q, r : Nat) -> LTE q r -> LTE (q+p) (r+p)` in this stdlib — the wrapper passed wildcards expecting a different shape. The stdlib already ships `Data.Nat.plusLteMonotone : LTE m n -> LTE p q -> LTE (m + p) (n + q)` — exactly what the wrapper was trying to be. Use it directly. 2. **Lifted both branches out of the `with`-block.** Inside `logSafeBounded s with (length s <= 8) proof cond`, Idris2 0.8.0 does not reduce `length "***"` (or `length "..."`) at type level — the goal is exposed as `LTE (integerToNat (prim__cast_IntInteger (prim__strLength (if True then ... else ...)))) 11` with the `if`-arm unreduced. Outside the with-block, the same lemmas type-check via Refl-equivalent computation. Extracted `logSafeBoundedShort` / `logSafeBoundedLong` as private helpers; the with-block now just dispatches. 3. **Right-associated the long-path proof.** `(++)` is right-associative in Idris (`a ++ b ++ c = a ++ (b ++ c)`), but the original composed `(p1 ++ "...") ++ p3` (left-associative) — its conclusion didn't match the with-block's False-branch goal. New proof: `step23 (length "..." + length (substr ...) ≤ 3 + 4)` then `step123 (length (substr 0 4 s) + length (...) ≤ 4 + 7)`. Plus two bound-name typos in `toLogSafeShortEq` / `toLogSafeLongEq` where the outer `_` discarded the `prf` argument that the inner branch then referenced. ## Test plan Per-module `idris2 --check` on every safety module — **all 12 green**: ``` Boj.SafetyLemmas green Boj.SafeAPIKey green (this PR) Boj.Safety green Boj.APIContractCoverage green Boj.CartridgeDispatch green Boj.Catalogue green Boj.CredentialIsolation green Boj.Federation green Boj.SafeCORS green Boj.SafeHTTP green Boj.SafePromptInjection green Boj.SafeWebSocket green ``` `idris2 --build src/abi/boj.ipkg` does not complete in 9 min on my dev box (gets SIGTERMed) — same note as #108. Per-module `--check` is the verification surface. ## Why this matters `PROOF-NEEDS.md`'s 2026-05-18 audit claims `SafeAPIKey` carries constructive proofs that close BJ2-partial. Pre-this-PR the proof did not compile. The audit was, like the SafetyLemmas axiom count noted in #108, evidently a desk-read of the source rather than a build. With this PR merged, the audited safety surface is buildable from scratch. Refs epic #87 Tier C (companion to #108 / #109). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 15d385c commit 1e60f60

1 file changed

Lines changed: 59 additions & 21 deletions

File tree

src/abi/Boj/SafeAPIKey.idr

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -145,23 +145,74 @@ sufficientEntropyNonEmpty (MkSufficientEntropy s {prf}) with (unpack s) proof up
145145
-- Works because toLogSafe is transparent and `if True then x else y = x`.
146146
private
147147
toLogSafeShortEq : (s : String) -> (length s <= 8 = True) -> toLogSafe s = "***"
148-
toLogSafeShortEq s _ with (length s <= 8)
148+
toLogSafeShortEq s prf with (length s <= 8)
149149
toLogSafeShortEq _ _ | True = Refl
150150
toLogSafeShortEq _ prf | False = absurd prf
151151

152152
-- Reduce toLogSafe s to the concat form when the long-path condition holds.
153153
private
154154
toLogSafeLongEq : (s : String) -> (length s <= 8 = False) ->
155155
toLogSafe s = substr 0 4 s ++ "..." ++ substr (length s `minus` 4) 4 s
156-
toLogSafeLongEq s _ with (length s <= 8)
156+
toLogSafeLongEq s prf with (length s <= 8)
157157
toLogSafeLongEq _ prf | True = absurd prf
158158
toLogSafeLongEq _ _ | False = Refl
159159

160-
-- Monotonicity of + over LTE (derived from Data.Nat primitives).
160+
-- Length bound on the redacted-short marker. Lifted out of `logSafeBounded`'s
161+
-- with-block so the elaborator reduces `length "***"` against the witness
162+
-- `LTE 3 11`. Inside the with-block the reduction does not fire on Idris2
163+
-- 0.8.0 (the goal is exposed as
164+
-- `LTE (integerToNat (prim__cast_IntInteger (prim__strLength (if ...))))`
165+
-- and the `if`-arm is not reduced before unification).
161166
private
162-
plusLteMonotone : {m, n, p, q : Nat} -> LTE m n -> LTE p q -> LTE (m + p) (n + q)
163-
plusLteMonotone lmn lpq =
164-
lteTransitive (plusLteMonotoneRight _ lmn) (plusLteMonotoneLeft _ lpq)
167+
shortMarkerBounded : LTE (length "***") 11
168+
shortMarkerBounded = LTESucc (LTESucc (LTESucc (LTEZero {right = 8})))
169+
170+
-- Length bound on the long-path ellipsis marker. Same lift-out reason as
171+
-- `shortMarkerBounded`.
172+
private
173+
ellipsisLen3 : LTE (length "...") 3
174+
ellipsisLen3 = LTESucc (LTESucc (LTESucc LTEZero))
175+
176+
-- Short path of `logSafeBounded`, lifted out of the with-block. The return
177+
-- type uses the *literal* `"***"` rather than `toLogSafe s`, which matches
178+
-- what the with-block's True-branch substitution rewrites the goal to.
179+
private
180+
logSafeBoundedShort : (s : String) -> (length s <= 8 = True) ->
181+
LTE (length "***") 11
182+
logSafeBoundedShort _ _ = shortMarkerBounded
183+
184+
-- Long path of `logSafeBounded`, lifted out of the with-block. The return
185+
-- type carries the substr-form directly (matching what the with-block False-
186+
-- branch rewrites the goal to via `if False`-evaluation of `toLogSafe`). The
187+
-- substr arguments are repeated rather than let-bound because Idris2 0.8.0's
188+
-- elaborator doesn't always propagate `let`-binding equalities through
189+
-- `appendLengthSum`'s implicit arguments. The proof is right-associated to
190+
-- match `++`'s associativity (`a ++ b ++ c = a ++ (b ++ c)`).
191+
private
192+
logSafeBoundedLong : (s : String) -> (length s <= 8 = False) ->
193+
LTE (length (substr 0 4 s ++ "..." ++ substr (length s `minus` 4) 4 s)) 11
194+
logSafeBoundedLong s _ =
195+
let l1 : LTE (length (substr 0 4 s)) 4
196+
l1 = substrLengthBound s 0 4
197+
l3 : LTE (length (substr (length s `minus` 4) 4 s)) 4
198+
l3 = substrLengthBound s (length s `minus` 4) 4
199+
-- Length of "..." ++ p3
200+
eq23 : length ("..." ++ substr (length s `minus` 4) 4 s)
201+
= length "..." + length (substr (length s `minus` 4) 4 s)
202+
eq23 = appendLengthSum "..." (substr (length s `minus` 4) 4 s)
203+
step23 : LTE (length "..." + length (substr (length s `minus` 4) 4 s)) (3 + 4)
204+
step23 = plusLteMonotone ellipsisLen3 l3
205+
l23 : LTE (length ("..." ++ substr (length s `minus` 4) 4 s)) 7
206+
l23 = replace {p = \n => LTE n 7} (sym eq23) step23
207+
-- Length of p1 ++ ("..." ++ p3)
208+
eq123 : length (substr 0 4 s ++ "..." ++ substr (length s `minus` 4) 4 s)
209+
= length (substr 0 4 s) + length ("..." ++ substr (length s `minus` 4) 4 s)
210+
eq123 = appendLengthSum (substr 0 4 s) ("..." ++ substr (length s `minus` 4) 4 s)
211+
step123 : LTE (length (substr 0 4 s) + length ("..." ++ substr (length s `minus` 4) 4 s)) (4 + 7)
212+
step123 = plusLteMonotone l1 l23
213+
l123 : LTE (length (substr 0 4 s ++ "..." ++ substr (length s `minus` 4) 4 s)) 11
214+
l123 = replace {p = \n => LTE n 11} (sym eq123) step123
215+
in l123
165216

166217
||| The redacted key from `toLogSafe` is always at most 11 characters long.
167218
|||
@@ -173,21 +224,8 @@ plusLteMonotone lmn lpq =
173224
export
174225
logSafeBounded : (s : String) -> LTE (length (toLogSafe s)) 11
175226
logSafeBounded s with (length s <= 8) proof cond
176-
logSafeBounded s | True =
177-
let eq = toLogSafeShortEq s cond
178-
in replace {p = \t => LTE (length t) 11} (sym eq)
179-
(LTESucc (LTESucc (LTESucc (LTEZero {right = 8}))))
180-
logSafeBounded s | False =
181-
let p1 = substr 0 4 s
182-
p3 = substr (length s `minus` 4) 4 s
183-
eq = toLogSafeLongEq s cond
184-
l1 = substrLengthBound s 0 4
185-
l3 = substrLengthBound s (length s `minus` 4) 4
186-
eq12 = appendLengthSum p1 "..."
187-
eq123 = appendLengthSum (p1 ++ "...") p3
188-
l12 = replace {p = \n => LTE n 7} (sym eq12) (plusLteMonotoneRight 3 l1)
189-
l123 = replace {p = \n => LTE n 11} (sym eq123) (plusLteMonotone l12 l3)
190-
in replace {p = \t => LTE (length t) 11} (sym eq) l123
227+
logSafeBounded s | True = logSafeBoundedShort s cond
228+
logSafeBounded s | False = logSafeBoundedLong s cond
191229

192230
--------------------------------------------------------------------------------
193231
-- FFI Bridge Declarations

0 commit comments

Comments
 (0)