Repository navigation
proof(SafeAPIKey): fix emptyKeyRejected + reshape fullMaskFormat (closes #95) - #96
Merged
Merged
Conversation
#95) Two distinct FFI-reduction issues in `src/Proven/SafeAPIKey/Proofs.idr` that fail under Idris2 0.8.0 (and were never caught by CI because the `Proofs` module was not in `proven.ipkg`): **emptyKeyRejected** — `mkAPIKey "" = Nothing`. The pre-fix definition funneled the empty string through `with (choose (length s >= 16))`, which can't reduce at type-check time because `prim__strLength` / `>=` are FFI primitives. Fix: add an explicit `mkAPIKey "" = Nothing` fast-path equation BEFORE the `with`-block. Identical runtime behaviour (empty rejected either way), and the proof now discharges by `Refl` directly via the first equation. **fullMaskFormat / Structure** — was `(key : APIKey) -> isPrefixOf "[REDACTED:" (fullMask key) = True`. The pre-fix `Refl` failed because `isPrefixOf` walks `unpack`/`++` through the String FFI on a universally-quantified `key`. Replaced with `fullMaskStructure : ... fullMask key = "[REDACTED:" ++ show (length key.raw) ++ " chars]"` — structural equality, definitionally true (`Refl`), and delivers the same safety guarantee: the output is a fixed-prefix template containing only the redacted length count, no key material. Any consumer that needs the original `isPrefixOf` form can derive it locally from `fullMaskStructure` + a small list-prefix lemma without running into the FFI-reduction wall. CI coverage closed: `Proven.SafeAPIKey.Proofs` added to `proven.ipkg` modules list. This was the gating condition from #92's sibling-audit acceptance criteria (was excluded by #94 pending the present fix). Verified: `idris2 --check src/Proven/SafeAPIKey/Proofs.idr` clean under Idris2 0.8.0 (zero `believe_me`, zero `assert_total`, zero `postulate`). Closes #95 Refs #92 (sibling-audit surfaced this) Refs #94 (which excluded SafeAPIKey.Proofs from ipkg pending this fix) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath
enabled auto-merge (squash)
May 30, 2026 14:15
|
🔍 Hypatia Security ScanFindings: 383 issues detected
View findings[
{
"reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
"type": "unpinned_action",
"file": "governance.yml",
"action": "pin_sha",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in architecture-enforcement.yml",
"type": "unknown",
"file": "architecture-enforcement.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in architecture-enforcement.yml",
"type": "unknown",
"file": "architecture-enforcement.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in boj-build.yml",
"type": "unknown",
"file": "boj-build.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in casket-pages.yml",
"type": "unknown",
"file": "casket-pages.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in casket-pages.yml",
"type": "unknown",
"file": "casket-pages.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in cflite_batch.yml",
"type": "unknown",
"file": "cflite_batch.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in cflite_pr.yml",
"type": "unknown",
"file": "cflite_pr.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in codeql.yml",
"type": "unknown",
"file": "codeql.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
},
{
"reason": "Issue in dogfood-gate.yml",
"type": "unknown",
"file": "dogfood-gate.yml",
"action": "flag",
"rule_module": "workflow_audit",
"severity": "medium"
}
]Powered by Hypatia Neurosymbolic CI/CD Intelligence |
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.



Summary
Closes #95 — the two SafeAPIKey `Proofs.idr` Refl proofs that were broken under Idris2 0.8.0 (a DIFFERENT bug class from #92's shadowing) now both type-check.
Root causes
Both proofs failed because Idris2 0.8.0 does not reduce string FFI primitives at type-check time.
`emptyKeyRejected : mkAPIKey "" = Nothing`
Pre-fix `mkAPIKey` definition:
```idris
mkAPIKey s with (choose (length s >= MinKeyLength))
mkAPIKey s | Left prf = Just (MkAPIKey s ...)
mkAPIKey s | Right _ = Nothing
```
For `mkAPIKey ""`, the type-checker needed `length ""` to reduce to a concrete `Nat`, then `0 >= 16` to `False`, then `choose False` to `Right Oh`. The `prim__strLength`/`>=` FFI chain doesn't reduce.
`fullMaskFormat : (key : APIKey) -> isPrefixOf "[REDACTED:" (fullMask key) = True`
`fullMask key` is `"[REDACTED:" ++ show (length key.raw) ++ " chars]"`. `isPrefixOf` goes via `unpack` + list-prefix-by — over a universally-quantified `key`, these don't reduce.
Fixes
`emptyKeyRejected` — fast-path equation
Added an explicit `mkAPIKey "" = Nothing` equation BEFORE the `with`-block in `SafeAPIKey.idr`:
```idris
mkAPIKey "" = Nothing
mkAPIKey s with (choose (length s >= MinKeyLength))
...
```
Identical runtime behaviour (empty rejected either way), proof discharges by `Refl` directly via the first equation match.
`fullMaskFormat` -> `fullMaskStructure` — reshape
Replaced with the structurally-true variant:
```idris
fullMaskStructure : (key : APIKey) ->
fullMask key = "[REDACTED:" ++ show (length key.raw) ++ " chars]"
fullMaskStructure _ = Refl
```
Same safety guarantee, mechanically: the masked output is a fixed-prefix template containing only the redacted length count, no key material. A consumer that needs `isPrefixOf` form can derive it locally from `fullMaskStructure` + a small list-prefix lemma without hitting the FFI wall.
CI gap closed
`Proven.SafeAPIKey.Proofs` added to `proven.ipkg` modules list — was excluded by #94 (sibling-audit) pending the present fix.
Acceptance criteria
Test plan
Refs
🤖 Generated with Claude Code