let-fate-decide: replace os.urandom with secrets.randbelow#125
Merged
Conversation
Switch card selection from hand-rolled secure_randbelow(os.urandom) to stdlib secrets.randbelow(). Update all docstrings, SKILL.md, and README.md references. Add 25-test suite covering deck construction, shuffle invariants, CLI validation, and migration regression checks.
- Bump version in plugin.json and marketplace.json (1.0.0 → 1.1.0) - Update SKILL.md to say "via secrets.randbelow()" instead of claiming the script itself implements rejection sampling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Incorporates unique changes from Scott's closed PR #128: - Broadened trigger patterns (casual delegation, YOLO, shrug-like brevity, expanded Yu-Gi-Oh references) in README.md and SKILL.md - Improved SKILL.md frontmatter description with casual/playful tone guidance and prefer-over-ask-questions-if-underspecified note - is_reversed() uses secrets.randbits(1) (more idiomatic coin flip) Additional fixes from vivisect analysis: - Converted MAJOR_ARCANA, RANKS, SUITS from lists to tuples (prevents silent deck corruption if imported as library) - Added type guard in draw() rejecting non-int/bool inputs - Added test_constants_are_immutable and test_draw_rejects_non_int Co-Authored-By: Scott Arciszewski <scott.arciszewski@trailofbits.com>
tob-scott-a
previously approved these changes
Mar 19, 2026
…fbits/skills into let-fate-decide/secrets-randbelow
tob-scott-a
approved these changes
Mar 20, 2026
dguido
approved these changes
Mar 31, 2026
Member
dguido
left a comment
There was a problem hiding this comment.
Review: let-fate-decide secrets.randbelow migration
Verdict: Approve
Summary
Clean migration from a custom secure_randbelow() (manual rejection sampling over os.urandom()) to stdlib secrets.randbelow() and secrets.randbits(). This is the right call -- secrets.randbelow() does the same rejection sampling internally, so the custom implementation was redundant.
What was checked
- No merge conflicts with main
- Versions match: plugin.json and marketplace.json both at 1.1.0
- YAML frontmatter: valid, name is kebab-case, description is third-person with trigger phrases
- No hardcoded paths
- All 27 tests pass (deck construction, shuffle properties, CLI integration, regression checks for the migration)
- Codex skills validation passes
- Plugin metadata validation passes
- ruff clean on both Python files
Code quality notes
- Fisher-Yates shuffle is correctly implemented with
secrets.randbelow(i + 1) secrets.randbits(1)for the coin flip is cleaner thanos.urandom(1)[0] & 1- Constants converted from lists to tuples (immutability) with matching test
- Type guard on
draw(n)rejecting bools and non-ints is a nice touch - Test suite is thorough: covers deck structure, shuffle invariants, randomness smoke tests, CLI args, and regression tests for the
os-to-secretsmigration
Minor observations (non-blocking)
draw(0)silently returns[]at the library level but the CLI rejects0as out-of-range. This is fine since the CLI has its own validation layer, but worth noting the asymmetry.draw(-5)also silently returns[](viarange(min(-5, 78))being empty). Same reasoning applies -- CLI guards against it.- The test runner's
except AssertionErroron line 235 would silently swallow test failures if there were a typo, but it's spelled correctly and all tests pass. For future-proofing, pytest compatibility (already works viauv run pytest) is the safer runner.
tob-joe
added a commit
that referenced
this pull request
Apr 14, 2026
Adds two tests that would have caught the PR #125 regression fixed by this PR (#144): - test_cli_content_flag: runs `draw_cards.py --content 2` end-to-end, exercising the os.path calls in draw() that NameError'd without `import os`. - test_os_imported_when_referenced: static guard that fails if draw_cards.py references `os.` without importing os. Verified: both tests fail against the pre-#144 draw_cards.py (along with most of the existing draw()/CLI tests — suggesting the test suite was not actually running in CI when #125 merged; worth a follow-up to wire it up). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
tob-joe
added a commit
that referenced
this pull request
Apr 14, 2026
Adds test_cli_content_flag, which runs `draw_cards.py --content 2` end-to-end -- the exact invocation path the agent uses -- and checks the output JSON includes real card content (not a "file not found" placeholder). This exercises the os.path calls in draw() that NameError'd between PR #125 and PR #144. Verified: fails against the pre-#144 draw_cards.py with "name 'os' is not defined"; passes on the fix. Note: most of the existing draw()/CLI tests in test_draw_cards.py also fail on the broken version, so #125 would have been caught at merge time if test_draw_cards.py were wired into CI. It isn't -- lint.yml runs pre-commit and bats, validate.yml checks frontmatter, neither runs Python tests. Wiring it up is a separate change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tob-joe
added a commit
that referenced
this pull request
Apr 14, 2026
Discovers test_*.py / *_test.py files under plugins/ and executes each one as a script. Matches the style of the existing bats job. Works today for: - plugins/constant-time-analysis/ct_analyzer/tests/test_analyzer.py - plugins/let-fate-decide/.../scripts/test_draw_cards.py Both test files already exist in the repo but no CI job invoked them. As a result, PR #125 (which broke let-fate-decide by removing `import os` while leaving `os.path` calls in draw()) merged with green CI even though the existing `test_draw_*` / `test_cli_*` tests would have caught it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 tasks
tob-joe
added a commit
that referenced
this pull request
Apr 15, 2026
Discovers test_*.py / *_test.py files under plugins/ and executes each one as a script. Matches the style of the existing bats job. Works today for: - plugins/constant-time-analysis/ct_analyzer/tests/test_analyzer.py - plugins/let-fate-decide/.../scripts/test_draw_cards.py Both test files already exist in the repo but no CI job invoked them. As a result, PR #125 (which broke let-fate-decide by removing `import os` while leaving `os.path` calls in draw()) merged with green CI even though the existing `test_draw_*` / `test_cli_*` tests would have caught it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dguido
added a commit
that referenced
this pull request
Apr 16, 2026
) * ci: add python-tests job to run plugin Python test suites Discovers test_*.py / *_test.py files under plugins/ and executes each one as a script. Matches the style of the existing bats job. Works today for: - plugins/constant-time-analysis/ct_analyzer/tests/test_analyzer.py - plugins/let-fate-decide/.../scripts/test_draw_cards.py Both test files already exist in the repo but no CI job invoked them. As a result, PR #125 (which broke let-fate-decide by removing `import os` while leaving `os.path` calls in draw()) merged with green CI even though the existing `test_draw_*` / `test_cli_*` tests would have caught it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci(python-tests): install aarch64 cross toolchain for constant-time-analysis TestCrossArchitecture.test_cross_compile_arm64 invokes clang with --target=aarch64-unknown-linux-gnu, which needs the aarch64 libc headers. Without them clang fails with: fatal error: 'bits/libc-header-start.h' file not found Install gcc-aarch64-linux-gnu + libc6-dev-arm64-cross so clang can find the cross headers. Also install clang explicitly since the runner may not have it preinstalled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: clarify python-tests workflow comments Resolves code review findings on PR #147 (comment accuracy only — no behavior change): - Rewrite the toolchain dependencies comment. The previous wording ("aarch64 cross gcc pulls in libc6-dev-arm64-cross") implied a transitive dependency, but `--no-install-recommends` suppresses Recommends, so libc6-dev-arm64-cross is installed only because it is listed explicitly. New comment names what each package supplies. - Document why `set -uo pipefail` deliberately omits -e (the loop collects per-file failures and exits with a combined code). Reviewers (codex + gemini + pr-review-toolkit agents) flagged 13 findings total; 11 were dismissed (false positives, design choices matching the bats job, or speculative). Quality pipeline (actionlint, zizmor, shellcheck, pre-commit, plugin validators, both Python test suites) all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Dan Guido <dan@trailofbits.com>
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
secure_randbelow(os.urandom)with stdlibsecrets.randbelow()per @opal-escent's feedbacksecrets.randbelow)secrets.randbelow(2)for card reversal coin flips instead ofos.urandom(1)[0] & 1os.urandom()tosecrets.randbelow()Test plan
python3 test_draw_cards.py)os.urandomorimport osremain in draw_cards.pysecure_randbelowfunction fully removedcc @opal-escent for review
🤖 Generated with Claude Code