gabriel / muse public
feat patch hub task/assignee-flag-and-tests #1 / 1
gabriel · 147 days ago · Apr 15, 2026 · Diff

feat(hub): add --assignee to issue create + _validate_assignee with full input hardening

Adds --assignee USER flag to 'muse hub issue create'. The flag validates the handle before any network I/O using a new _validate_assignee() helper, then dispatches a second POST to /assign after the issue is created.

_validate_assignee() rejects: null bytes, control chars (terminal injection), newlines (output spoofing), non-ASCII Unicode (confusable impersonation), shell metacharacters, oversized handles (>64 chars), and anything not matching ^[a-zA-Z0-9][a-zA-Z0-9_-]*$. allow_empty=True permits empty string for the unassign path (issue assign and issue update --assign).

Validation also added to run_issue_assign() and run_issue_update() --assign path, which previously sent handles straight to the API without any checks.

Also adds: import re, _MAX_HANDLE_LEN and _HANDLE_RE module constants. Tests: 86 tests across all 8 tiers (shape, round-trip, edge cases, stress, data integrity, performance, security, docstrings).

sha256:2dbefd99447c2a2a4896a2952e3d6a1d4e27d3eaac0078b352fa26c0db35f070 sha
+96 ~4 symbols
sha256:9b5712c671c9f68445c55e3b1b4d7f15b0e3423d050ee51eedb838d1b6abd1ad snapshot
+96
symbols added
~4
symbols modified
0
dead code introduced
Semantic Changes 100 symbols
~ tests/test_hub_body_file_assignee.py .py 92 symbols added
+ TestDataIntegrity class class TestDataIntegrity L488–574
+ test_assign_called_with_correct_issue_number method method test_assign_called_with_correct_issue_number L547–574
+ test_assignee_payload_matches_flag_value method method test_assignee_payload_matches_flag_value L491–514
+ test_body_file_bytes_match_payload_exactly method method test_body_file_bytes_match_payload_exactly L516–545
+ TestDocstrings class class TestDocstrings L788–840
+ test_handle_re_constant_is_named_consistently method method test_handle_re_constant_is_named_consistently L831–834
+ test_max_handle_len_constant_documented_in_code method method test_max_handle_len_constant_documented_in_code L836–840
+ test_resolve_body_docstring_has_args_section method method test_resolve_body_docstring_has_args_section L803–804
+ test_resolve_body_docstring_has_raises_section method method test_resolve_body_docstring_has_raises_section L809–810
+ test_resolve_body_docstring_has_returns_section method method test_resolve_body_docstring_has_returns_section L806–807
+ test_resolve_body_docstring_mentions_body_file method method test_resolve_body_docstring_mentions_body_file L795–796
+ test_resolve_body_docstring_mentions_stdin method method test_resolve_body_docstring_mentions_stdin L798–801
+ test_resolve_body_has_docstring method method test_resolve_body_has_docstring L791–793
+ test_validate_assignee_docstring_has_raises_section method method test_validate_assignee_docstring_has_raises_section L825–826
+ test_validate_assignee_docstring_has_threat_model method method test_validate_assignee_docstring_has_threat_model L818–823
+ test_validate_assignee_docstring_mentions_allow_empty method method test_validate_assignee_docstring_mentions_allow_empty L815–816
+ test_validate_assignee_docstring_mentions_user_error method method test_validate_assignee_docstring_mentions_user_error L828–829
+ test_validate_assignee_has_docstring method method test_validate_assignee_has_docstring L812–813
+ TestEdgeCases class class TestEdgeCases L311–420
+ test_body_file_missing_exits_with_user_error method method test_body_file_missing_exits_with_user_error L349–359
+ test_body_file_wins_over_body_when_both_given method method test_body_file_wins_over_body_when_both_given L314–347
+ test_issue_create_assignee_shown_in_success_output method method test_issue_create_assignee_shown_in_success_output L375–387
+ test_resolve_body_body_file_none_returns_body_string method method test_resolve_body_body_file_none_returns_body_string L400–403
+ test_resolve_body_body_file_path_returns_file_contents method method test_resolve_body_body_file_path_returns_file_contents L405–412
+ test_resolve_body_body_only_returns_body method method test_resolve_body_body_only_returns_body L395–398
+ test_resolve_body_neither_body_nor_body_file_returns_empty method method test_resolve_body_neither_body_nor_body_file_returns_empty L389–393
+ test_resolve_body_stdin_sentinel method method test_resolve_body_stdin_sentinel L414–420
+ test_validate_assignee_empty_allow_empty_false_raises method method test_validate_assignee_empty_allow_empty_false_raises L365–369
+ test_validate_assignee_empty_allow_empty_true_ok method method test_validate_assignee_empty_allow_empty_true_ok L361–363
+ test_validate_assignee_single_char_valid method method test_validate_assignee_single_char_valid L371–373
+ TestPerformance class class TestPerformance L582–621
+ test_resolve_body_from_file_single_read method method test_resolve_body_from_file_single_read L585–606
+ test_validate_assignee_invalid_under_1ms method method test_validate_assignee_invalid_under_1ms L615–621
+ test_validate_assignee_valid_under_1ms method method test_validate_assignee_valid_under_1ms L608–613
+ TestRoundTrip class class TestRoundTrip L189–303
+ test_issue_assign_validates_handle_before_api_call method method test_issue_assign_validates_handle_before_api_call L291–303
+ test_issue_create_with_assignee_calls_create_then_assign method method test_issue_create_with_assignee_calls_create_then_assign L192–210
+ test_issue_create_with_body_file_sends_correct_body method method test_issue_create_with_body_file_sends_correct_body L228–258
+ test_issue_create_without_assignee_does_not_call_assign method method test_issue_create_without_assignee_does_not_call_assign L212–226
+ test_issue_update_with_body_file_sends_correct_body method method test_issue_update_with_body_file_sends_correct_body L260–289
+ TestSecurity class class TestSecurity L629–780
+ test_body_file_path_not_leaked_on_error method method test_body_file_path_not_leaked_on_error L733–751
+ test_control_characters_rejected method method test_control_characters_rejected L653–659
+ test_empty_handle_not_allowed_on_create method method test_empty_handle_not_allowed_on_create L702–716
+ test_extremely_long_handle_rejected method method test_extremely_long_handle_rejected L696–700
+ test_invalid_handle_rejects_before_network_io method method test_invalid_handle_rejects_before_network_io L718–731
+ test_issue_assign_invalid_handle_rejected method method test_issue_assign_invalid_handle_rejected L753–765
+ test_issue_update_invalid_assign_rejected_before_network method method test_issue_update_invalid_assign_rejected_before_network L767–780
+ test_non_ascii_unicode_rejected method method test_non_ascii_unicode_rejected L669–675
+ test_shell_metacharacters_rejected method method test_shell_metacharacters_rejected L690–694
+ TestShape class class TestShape L107–181
+ test_assignee_flag_present_on_issue_create method method test_assignee_flag_present_on_issue_create L130–136
+ test_body_file_flag_present_on_issue_comment method method test_body_file_flag_present_on_issue_comment L150–154
+ test_body_file_flag_present_on_issue_create method method test_body_file_flag_present_on_issue_create L138–142
+ test_body_file_flag_present_on_issue_update method method test_body_file_flag_present_on_issue_update L144–148
+ test_handle_re_is_compiled_pattern method method test_handle_re_is_compiled_pattern L120–123
+ test_handle_re_matches_valid_handles method method test_handle_re_matches_valid_handles L156–169
+ test_handle_re_rejects_at_symbol method method test_handle_re_rejects_at_symbol L177–178
+ test_handle_re_rejects_leading_hyphen method method test_handle_re_rejects_leading_hyphen L171–172
+ test_handle_re_rejects_slash method method test_handle_re_rejects_slash L180–181
+ test_handle_re_rejects_spaces method method test_handle_re_rejects_spaces L174–175
+ test_max_handle_len_positive_int method method test_max_handle_len_positive_int L125–128
+ test_resolve_body_importable method method test_resolve_body_importable L110–113
+ test_validate_assignee_importable method method test_validate_assignee_importable L115–118
+ TestStress class class TestStress L428–480
+ test_handle_one_over_max_rejected method method test_handle_one_over_max_rejected L436–441
+ test_issue_create_with_many_labels_and_assignee method method test_issue_create_with_many_labels_and_assignee L461–480
+ test_large_body_file_read_correctly method method test_large_body_file_read_correctly L443–451
+ test_max_length_valid_handle_accepted method method test_max_length_valid_handle_accepted L431–434
+ test_validate_assignee_called_1000_times_fast method method test_validate_assignee_called_1000_times_fast L453–459
+ _hub_patches function function _hub_patches L84–99
+ _setup_auth function function _setup_auth L72–81
+ cli variable variable cli L36–36
+ CliRunner import import CliRunner L23–23
+ ExitCode import import ExitCode L33–33
+ IdentityEntry import import IdentityEntry L34–34
+ _HANDLE_RE import import _HANDLE_RE L26–26
+ _MAX_HANDLE_LEN import import _MAX_HANDLE_LEN L26–26
+ __version__ import import __version__ L25–25
+ _resolve_body import import _resolve_body L26–26
+ _validate_assignee import import _validate_assignee L26–26
+ annotations import import annotations L14–14
+ json import import json L16–16
+ pathlib import import pathlib L17–17
+ pytest import import pytest L22–22
+ save_identity import import save_identity L34–34
+ set_hub_url import import set_hub_url L32–32
+ textwrap import import textwrap L18–18
+ time import import time L19–19
+ mock import import unittest.mock L20–20
+ repo function function repo L46–69
+ runner variable variable runner L37–37
~ muse/cli/commands/hub.py .py 4 symbols added, 4 symbols modified
+ _HANDLE_RE variable variable _HANDLE_RE L140–140
+ _MAX_HANDLE_LEN variable variable _MAX_HANDLE_LEN L135–135
+ _validate_assignee function function _validate_assignee L685–758
+ re import import re L88–88
← Older Oldest on task/assignee-flag-and-tests
All commits
Newer → Latest on task/assignee-flag-and-tests

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:2dbefd99447c2a2a4896a2952e3d6a1d4e27d3eaac0078b352fa26c0db35f070 --body "your comment"