-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Validate CLAUDE_CLI_VERSION and remove shell interpolation from the build scripts #1117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ae771e2
Validate CLAUDE_CLI_VERSION and avoid shell interpolation in download…
qing-ant 118bf7c
Validate the version argument to update_cli_version.py
qing-ant cd91b25
Fix two union-attr errors in download_cli.py retry loop
qing-ant 6764d44
Fail CI when the CLI install pipeline fails
qing-ant 7d23db2
Lint and typecheck scripts/ in CI
qing-ant d4a6529
Exit cleanly on an invalid CLAUDE_CLI_VERSION
qing-ant a6a912a
Match the installer's version grammar, and stop retrying its rejections
qing-ant 0b68217
Simplify the version validator and its install-path tests
qing-ant 87cc5d6
Stop guessing at installer argument rejections; tighten dist-tag matc…
qing-ant 35ed7ed
Fail fast on the install failures a retry cannot fix
qing-ant 54fd51c
Accept comment-based-help installers; simplify the validator and inst…
qing-ant 1676543
Fold the two install-path test harnesses into one
qing-ant a8c73e9
Cut the comment bloat, and share the install scaffolding across platf…
qing-ant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| r"""Shared validation for Claude Code CLI version strings. | ||
|
|
||
| Two scripts constrain the same value: update_cli_version.py writes it into | ||
| src/claude_agent_sdk/_cli_version.py, and download_cli.py (reached from | ||
| build_wheel.py) reads it back out and hands it to an installer. A second copy | ||
| of the rule would let the writer emit a value the reader rejects, so the | ||
| pattern and its validation helper live here once. | ||
|
|
||
| The installer is the authority on what a version may be. install.sh enforces | ||
|
|
||
| ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ | ||
|
|
||
| and install.ps1 enforces the same rule, so a value this module admits but the | ||
| installer does not is not a version -- it is an error we defer to install time, | ||
| where it surfaces behind a retry loop and a misleading "Error downloading CLI" | ||
| headline. VERSION_PATTERN therefore mirrors that grammar: three dot-separated | ||
| numeric components with an optional prerelease/build suffix, which covers both | ||
| releases ("2.1.207") and dev builds | ||
| ("2.1.146-dev.20260519.t105443.shaece3dab"). | ||
|
|
||
| We deliberately accept a strict *subset* of what the installer allows: the | ||
| installer's suffix is `-[^\s]+`, which would admit quotes, backslashes, | ||
| semicolons and every other non-space character, so the suffix here is narrowed | ||
| to the alphanumeric/dot/plus/hyphen set that real versions use. Never widen | ||
| this pattern back toward the installer's. | ||
|
|
||
| That narrowing is a security boundary, not just input hygiene: | ||
|
|
||
| * update_cli_version.update_cli_version() writes the version into a Python | ||
| string literal in a real source file, so it must never admit a double | ||
| quote, a backslash, or a newline. | ||
| * download_cli.download_cli() hands the version to an installer. Neither of | ||
| its paths interpolates it into a command string -- Unix passes it as its | ||
| own argv element, Windows passes it in the environment -- so for that | ||
| caller the allowlist is defense in depth rather than the only barrier. | ||
|
|
||
| "latest" and "stable" are the installer's dist-tags. Both are *moving*: they | ||
| resolve to whatever build is current at install time. That is fine for a | ||
| download, and wrong for a pin -- _cli_version.py is the only record of which | ||
| build went into the wheels, so it must name one concrete build. Hence | ||
| ``allow_dist_tag``. | ||
|
|
||
| Widening any of this requires re-reading tests/test_download_cli.py and | ||
| tests/test_update_cli_version.py. | ||
|
|
||
| VERSION_PATTERN is deliberately unanchored, and matched with fullmatch() | ||
| rather than match(): with "^...$" a swap to match() would silently accept a | ||
| trailing newline ("1.0.0\n"); unanchored, the same swap accepts obvious | ||
| prefixes like "1.0.0; id" and fails immediately in tests. | ||
| """ | ||
|
|
||
| import re | ||
|
|
||
| # A concrete version: MAJOR.MINOR.PATCH with an optional suffix. The suffix is | ||
| # the installer's `-[^\s]+` narrowed to characters that appear in real | ||
| # versions -- see the module docstring. | ||
| VERSION_PATTERN = re.compile(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.+-]+)?") | ||
|
qing-ant marked this conversation as resolved.
|
||
|
|
||
| # The moving tags the installer resolves at install time. Compared lowercased, | ||
| # so "LATEST" is the sentinel rather than a mysterious "concrete version". | ||
| DIST_TAGS = ("latest", "stable") | ||
|
|
||
| # Anything word-shaped that is not a version: "next", "beta", "nightly". Named | ||
| # so the error can say *why* it was rejected instead of printing a regex. | ||
| _DIST_TAG_SHAPED = re.compile(r"[A-Za-z][0-9A-Za-z-]*") | ||
|
|
||
| _SUPPORTED_TAGS = ", ".join(repr(tag) for tag in DIST_TAGS) | ||
|
|
||
|
|
||
| def _expected(allow_dist_tag: bool) -> str: | ||
| """The phrase naming what the caller should have passed instead.""" | ||
| if allow_dist_tag: | ||
| return f"{_SUPPORTED_TAGS}, or a concrete version" | ||
| return "a concrete version" | ||
|
|
||
|
|
||
| def validate_version(version: str, *, source: str, allow_dist_tag: bool) -> str: | ||
| """Return the usable form of ``version``, or raise. | ||
|
|
||
| Surrounding whitespace is stripped before anything else: a trailing "\\n" | ||
| from a file read, a "\\r" from a CRLF checkout, or a stray space from YAML | ||
| is unambiguous in intent, and the stripped value is what the caller gets | ||
| back and must use downstream. | ||
|
|
||
| Args: | ||
| version: The candidate version string. | ||
| source: Name of where the value came from, used in the error message | ||
| (e.g. "CLAUDE_CLI_VERSION"). | ||
| allow_dist_tag: Whether a moving dist-tag ("latest", "stable") is | ||
| acceptable. It is for a download, which resolves it at install | ||
| time; it is not for a value pinned into _cli_version.py, which must | ||
| name the one concrete build that went into the wheels. | ||
|
|
||
| Returns: | ||
| The stripped version, with a dist-tag normalized to lowercase. | ||
|
|
||
| Raises: | ||
| ValueError: If ``version`` is neither an allowed dist-tag nor a | ||
| fullmatch of VERSION_PATTERN. | ||
| """ | ||
| candidate = version.strip() | ||
|
|
||
| # A dist-tag fails VERSION_PATTERN, so it is recognized by name -- and | ||
| # case-insensitively, so "LATEST" is not mistaken for something else. | ||
| if candidate.lower() in DIST_TAGS: | ||
| if allow_dist_tag: | ||
| return candidate.lower() | ||
| raise ValueError( | ||
| f"Invalid {source}: {candidate!r} is a moving dist-tag, not a concrete " | ||
| f"version. A pinned version must name the one build that goes into the " | ||
| f"wheels. Expected a version matching {VERSION_PATTERN.pattern}" | ||
| ) | ||
|
|
||
| if VERSION_PATTERN.fullmatch(candidate): | ||
| return candidate | ||
|
|
||
| # Rejected from here on; what is left is choosing the most useful reason. | ||
|
|
||
| # "v2.1.207" is the single most likely typo, and the installer rejects it. | ||
| # Say so, rather than printing the pattern and leaving the reader to spot | ||
| # the leading "v". Not normalized away: the caller asked for something we | ||
| # do not support, and silently installing a different string is worse. | ||
| if candidate[:1] in ("v", "V") and VERSION_PATTERN.fullmatch(candidate[1:]): | ||
| raise ValueError( | ||
| f"Invalid {source}: {candidate!r}. " | ||
| f"Did you mean {candidate[1:]!r}? (no leading 'v')" | ||
| ) | ||
|
|
||
| if _DIST_TAG_SHAPED.fullmatch(candidate): | ||
| raise ValueError( | ||
| f"Invalid {source}: {candidate!r} is not a supported dist-tag; " | ||
| f"use {_expected(allow_dist_tag)}" | ||
| ) | ||
|
|
||
| raise ValueError( | ||
| f"Invalid {source}: {version!r}. " | ||
| f"Expected {_expected(allow_dist_tag)} matching {VERSION_PATTERN.pattern}" | ||
| ) | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.