Repository navigation
release: prepare v2.13.0 security and blocking fixes - #833
Merged
Merged
Conversation
#604) * fix(analyzer): flag settings keys that run commands or reroute traffic The bundled_execution_surface analyzer only inspected hooks and permissions, so a skill whose only risky content was a settings key like apiKeyHelper scanned as 0/SAFE. Add rule BH4 for the seven settings keys that run shell commands once the settings are active (apiKeyHelper, awsAuthRefresh, awsCredentialExport, gcpAuthRefresh, otelHeadersHelper, and type:command statusLine/fileSuggestion). statusLine and fileSuggestion stay suppressed when disableAllHooks is effective, matching the settings reference; the auth and telemetry helpers run regardless. Extend BH3 to the settings-level surface: env ANTHROPIC_BASE_URL that reroutes model traffic (HIGH), enableAllProjectMcpServers (CRITICAL) and non-empty enabledMcpjsonServers (HIGH) which approve project MCP servers without the per-server prompt. Closes #603. Signed-off-by: Deepak Jain <deepujain@gmail.com> * fix(analyzer): flag documented env execution/redirect variables in bundled settings The env scan covered only ANTHROPIC_BASE_URL, so the other documented execution and redirect variables from issue 603 scanned as safe. Add a bounded classifier over the documented names: alternate Anthropic base URLs and proxy URLs (flagged when they reroute to a non-default or remote host), CLAUDE_CODE_SHELL_PREFIX, NODE_OPTIONS loader-injection flags (--require/--import/--loader), LD_PRELOAD, DYLD_INSERT_LIBRARIES, and GIT_SSH_COMMAND. Positive tests flag malicious values and benign-value controls stay silent (empty values, the default base URL, local proxies, non-loader NODE_OPTIONS flags). Co-authored-by: Deepak <deepujain@users.noreply.github.com> Signed-off-by: Deepak Jain <deepujain@gmail.com> --------- Signed-off-by: Deepak Jain <deepujain@gmail.com> (cherry picked from commit 89e9087) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
* fix(patterns): detect constructed sensitive paths
Signed-off-by: Deepak Jain <deepujain@gmail.com>
* fix(patterns): preserve shared Python AST cache
Signed-off-by: Deepak Jain <deepujain@gmail.com>
* fix(patterns): resolve constructed joins via AST with per-line PE3 dedup
The PE3 constructed-path recognizer missed multiline and imported/aliased
os.path.join spellings, and emitted a second PE3 for lines the pattern loop
already covered. Resolve literal join calls from the Python AST (handling
from/import aliases), and keep the best-confidence PE3 per line.
Signed-off-by: Deepak Jain <deepujain@gmail.com>
* fix(patterns): gate constructed-path AST parse behind join pre-check
The AST-based os.path.join resolver parsed every Python file, defeating
the runner's parse-once invariant (one shared parse per file across the
analyzer fan-out). Gate the local parse behind a cheap textual pre-check
so files without a plausible join() call never pay for it. The module
stays lexical, preserving windowed and normalized-view scans for all
file types.
Signed-off-by: Deepak Jain <deepujain@gmail.com>
* fix(patterns): alias-aware join gate, shared AST reuse, span dedup
Address rng1995's review on constructed sensitive-path detection:
- Broaden the join pre-check to cover imported local names: the gate now
matches every name the file binds to os.path or os.path.join, so
'from os.path import join as j' followed by j('/etc', 'passwd') reaches
alias resolution instead of returning early on the missing literal join(.
- Route the supplemental analysis through the scan's shared AST: node()
publishes the runner's python_ast_cache_key via a contextvar and
_constructed_sensitive_paths consults it with the new peek_python_ast
helper (exact-content hit only, never parses or writes). The module stays
lexical so windowed and normalized views keep running; windowed view
fragments are skipped and standalone callers keep one on-demand parse.
- Deduplicate across the call's whole source span: hits now carry
(start_line, end_line), so a join call wrapped over several lines no
longer produces a second PE3 when the lexical loop fires on a wrapped
argument line.
Tests: extend test_pe3_aliased_join_imports_are_detected with the renamed
import case, add test_pe3_multiline_constructed_join_is_deduplicated, and
extend test_graph_scan_parses_python_once_before_parallel_analyzers with a
literal os.path.join call to verify the graph still performs one parse.
Signed-off-by: Deepak Jain <deepujain@gmail.com>
* fix(patterns): restore constructed-path findings on windowed views
Above SECURITY_VIEW_WINDOW_CHARS the runner hands lexical modules window
slices instead of the whole file, so peek_python_ast never hits the scan's
whole-file cache entry and the constructed-path analysis silently dropped
its findings (regression introduced in 56d121c; 40cea88 reported them).
On a peek miss, parse the provided content directly behind the same plain
textual join gate, restoring the 40cea88 behavior for window slices while
keeping the 17:45 improvements for whole files: alias-aware gate, shared
AST reuse, multiline span dedup, and scan-count discipline (fragments
without a plausible join call never parse).
Regression tests:
- unit: a windowed fragment under a scan keeps PE3 coverage, and a
join-free fragment triggers no parse
- graph: a ~376k-char file keeps its constructed-path PE3 through the
full scan
Signed-off-by: Deepak Jain <deepujain@gmail.com>
* fix(patterns): recognize renamed join imports on windowed cache misses
The windowed-fragment fallback gated on the plain join( spelling, so a
fragment calling j( bound by from os.path import join as j never parsed
and silently dropped its constructed-path PE3 (review P1).
The parse gate now also fires on from os.path import spellings: the
fragment is parsed to collect its import aliases, and the existing
alias-aware join gate filters import-only fragments. Fragments without
a plausible join call or join import still never parse.
Regression tests:
- graph: the ~376k-char large-file regression now spells the call
through the renamed import with trailing comment padding
- unit: a windowed fragment with from os.path import join as j keeps
its PE3 coverage under a scan
Signed-off-by: Deepak Jain <deepujain@gmail.com>
* fix(patterns): carry whole-file join aliases into windowed fragments
A renamed os.path.join import (from os.path import join as j) and its call
site can fall in different runner windows. The call's fragment carries no
import aliases of its own and never spells join(, so the textual gate
skipped it before alias resolution and the constructed sensitive path was
silently dropped.
Carry the whole file's import-alias map from the scan cache into the
fragment analysis: peek the whole-file parse by filename (no content
match), use its aliases for the join pre-check gate, the alias-aware gate,
and call resolution. Fragments of the same scanned file share its import
bindings, so the renamed call resolves to os.path.join in the later
window. Standalone and exact-cache-hit paths are unchanged, and fragments
without a plausible join call or join import still never parse.
Adds two regression tests: an analyze-level fragment whose import lives in
another window, and a full node() graph regression with import and call
separated across real runner windows.
Signed-off-by: deepujain <deepujain@gmail.com>
* fix(patterns): evaluate windowed join calls on the whole-file AST
The windowed fallback parsed each fragment as a standalone module, so a
slice starting mid-block (for example inside a function body) failed
parsing and its constructed-path PE3 findings were silently dropped.
Evaluate the supplemental constructed calls against the whole file's
cached tree instead, reusing the existing cache peek: call spans map
onto fragment-relative lines and only calls starting inside the
fragment are owned by it, preserving full-file syntax, scope and
import aliases as well as the runner's source-coordinate and dedupe
behavior. Fragments that are not slices of the whole file (normalized
views) keep the standalone-parse fallback.
Add direct and graph regressions using the enclosing-function fixture:
a mid-function window now reports exactly one PE3 at the call site.
Signed-off-by: Deepak Jain <deepujain@gmail.com>
---------
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: deepujain <deepujain@gmail.com>
(cherry picked from commit 5d370aa)
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
* fix(discovery): report skipped symlinked skills Signed-off-by: Deepak Jain <deepujain@gmail.com> * fix(discovery): retain siblings beside symlink Signed-off-by: Deepak Jain <deepujain@gmail.com> * fix(discovery): skip ignored-name symlinks before recording limitation Address rng1995 review on #499 (2026-09-18): apply the hidden-name and _SKIP_DIRS exclusion before recording multi_skill_symlinked_entry, so an intentionally ignored .git/.venv/node_modules symlink no longer makes an otherwise complete scan incomplete. The limitation is retained for genuinely eligible symlinked children. Adds an ignored-name symlink regression. Signed-off-by: Deepak Jain <deepujain@gmail.com> * fix(discovery): restrict symlink dot-name exemption to _SKIP_DIRS Address rng1995 change request on #499 (review 5286279806): the blanket dot-name exemption in the symlink branch was broader than the discovery policy, silently excluding eligible dot-prefixed symlinked skills such as .review-helper even though regular .review-helper directories are supported as local-only skills. Restrict the pre-limitation exemption to the actual _SKIP_DIRS set so an eligible dot-prefixed symlink records the multi_skill_symlinked_entry limitation (complete == False) instead of being silently excluded. Adds a regression test pairing a .review-helper symlink with the ignored .git control: the eligible symlink is accounted while the .git symlink contributes no limitation of its own. Signed-off-by: Deepak Jain <deepujain@gmail.com> --------- Signed-off-by: Deepak Jain <deepujain@gmail.com> (cherry picked from commit 794eca9) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
* fix(build_context): strip leading BOMs before manifest frontmatter detection
decode_text() decodes with plain utf-8, which never strips a byte-order
mark, so a BOM-prefixed SKILL.md left content[0] == U+FEFF and the
content.startswith("---") sniff in _parse_manifest returned an empty
manifest. Every manifest-gated analyzer then early-exits as
not_applicable/manifest_absent, so LP1-LP4 and TP1-TP4 stop contributing
findings for that skill.
Strip leading BOMs in a loop, scoped to delimiter detection. decode_text()
itself is left alone because P2/TP1/P9 treat U+FEFF in file bodies as a
hidden-character injection signal.
Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com>
* fix(multi_skill): strip leading BOM before raw-byte frontmatter sniff
_extract_skill_name checks prefix.startswith(b"---") on raw bytes before
decoding, so a BOM-prefixed sub-skill fell through to the fallback and was
listed under its directory name instead of the name: declared in its
frontmatter. Byte-level counterpart to the same strip in
build_context._parse_manifest.
Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com>
* fix(excessive_agency): match leading BOM in SKILL.md frontmatter bounds
_frontmatter_bounds anchored on \A---, so a BOM-prefixed SKILL.md returned
None and EA5 silently reported no frontmatter model pin. This is the third
frontmatter sniff in the same class as the two already fixed on this branch.
The BOM is absorbed into the opening match rather than stripped, because the
offsets this function returns index the caller's original content: _ea5_findings
slices it and derives line numbers and context from it, so removing characters
would shift every reported position by one.
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
---------
Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
(cherry picked from commit 9af9f7d)
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…42574) (#473) * fix(P2): detect bidi control characters outside markdown files The bidi-control pattern (Trojan Source, CVE-2021-42574) lived in P2_PATTERNS, which only runs when file_type is markdown or other. static_runner maps .py to python, .sh to shell and so on, so a bidi payload in a bundled script was never checked - the file types the CVE actually targets. Evaluate the bidi pattern regardless of file_type, alongside the existing unconditional Unicode Tag-block check. Zero-width detection stays markdown-gated deliberately: ZERO_WIDTH_CHARS includes U+FEFF, so ungating it would flag every BOM-prefixed source file. Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com> * docs(prompt_injection): record why bidi direction marks are excluded Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> * fix(prompt_injection): import get_line_number used by the bidi scan The bidi-control block calls get_line_number at line 470 but the helper was never added to the .common import, so ruff reported F821 and the five bidi tests raised NameError at runtime. static_patterns_data_exfiltration.py already imports the same helper from .common and calls it with the same signature. Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com> Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> * fix(prompt_injection): bound the bidi scan and keep exact locations Moving bidi detection out of the markdown-gated P2 loop dropped what that loop supplied. The runtime callback was not passed to _p2_pattern_matches or checked per match, so a file with a bidi control on every line was enumerated to completion past the deadline. Each finding also rescanned the content for its line number instead of using the shared location index, which lost the columns and the complete-match identity. Restore the markdown loop's shape: pass check_runtime through, check it per emitted match, and build the finding with locations.location() and complete_match=match.group(0). get_line_number is no longer used here. Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com> Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> --------- Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com> Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> Co-authored-by: Narendran Raghavan <32655573+rng1995@users.noreply.github.com> (cherry picked from commit 79b855b) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…ource fix(ae1): keep reference-resolution caveats from flagging SKILL.md self-references (cherry picked from commit ec6633e) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…-ownership fix(analyzer): avoid false parse limits on quoted shell assignments (cherry picked from commit 7822cb1) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…e-limit fix(analyzer): stop value-only parameter expansions from marking files partial (cherry picked from commit 2226747) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…rrence fix(baseline): fingerprint every occurrence of a deduplicated finding (cherry picked from commit e5ceb61) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(analyzer): recognize Node's -r alias for --require in NODE_OPTIONS (cherry picked from commit c0de629) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…ositive fix(sc2): treat command-line program stdin as data (cherry picked from commit f665677) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(meta): bind assessments to exact findings (cherry picked from commit 088e3c3) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…executable fix: inspect TypeScript declaration content (cherry picked from commit 383077e) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(analyzer): analyze hooks declared in .claude-plugin/plugin.json (cherry picked from commit 351c09d) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(analyzer): avoid false shell parse limits in Markdown (cherry picked from commit cb458ff) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(analyzers): retain findings with unavailable Python AST columns (cherry picked from commit e3e268a) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(yara): make reverse-shell socket strings span newlines (cherry picked from commit 191773e) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…cking fix(analyzer): bound XOR literal and function-header parsing (cherry picked from commit 4d002b2) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(mcp): restrict unauthenticated HTTP listeners to loopback (cherry picked from commit 067f309) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(ci): pin the release checkout action to an immutable commit (cherry picked from commit 0c3f73d) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(cli): prevent report output from overwriting the input file (cherry picked from commit 548c9c3) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…-20261005 fix(batch): prevent Markdown report injection (cherry picked from commit 94abd7c) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(taint): collect taint before checking sinks so order-independent flows aren't dropped (cherry picked from commit 0d0757c) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…t-20261005 fix(report): contain untrusted Markdown fields (cherry picked from commit e2bf4a1) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…entials fix(mcp): isolate Git credentials for HTTP scans (cherry picked from commit d5e508f) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…ider fix(provider): fail closed for unsafe Codex CLI analysis (cherry picked from commit d3b8590) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(provider): replace the retired NVIDIA Build default (cherry picked from commit 81bf9db) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…onment fix(provider): allowlist agent CLI environment variables (cherry picked from commit 4a9f7b1) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…dentials fix(opencode): redact short credential values from scan output (cherry picked from commit 8562ecc) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(taint): scope taint to functions and bind call-site arguments (cherry picked from commit 530d3c5) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…ion-accounting fix(ledger): exclude scope boundaries from analyzer work (cherry picked from commit 40c8670) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(cli): preserve registry inputs when writing reports (cherry picked from commit 73a4a00) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…al-followup-20261006 fix(ci): keep release credentials out of the build checkout (cherry picked from commit 665bb56) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
The selected #691 fix requires the shared structured-output binding helpers introduced by #529/#708. Retain only their generic method selection, unforced-tool validation and CLI adapter method compatibility. Provider registry opt-ins and the unrelated provider/model features remain excluded. The selected #578 fix also uses prepared-analysis dispatch, absolute source-coordinate mapping and whitespace-continuity scaffolding from #513. These shared runner helpers were included in its selective backport; the P6 heading detector, static-only provider-provenance refactor, #645 license handling, #682 triage tags and #784 Python string ownership remain excluded. Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
The selected baseline and MCP regression requires static scans, including fresh server imports, to work without a configured LLM provider. Backport the provider-neutral constants guard, availability handling and static provenance capture from #513 while preserving rejection of unknown providers when LLM analysis is requested. The P6 detection changes remain excluded. Make the generic unforced-tool test choose function_calling explicitly instead of depending on the excluded #708 spark model metadata. Apply required Ruff import ordering and formatting. Isolated lint and format checks pass; runtime verification will be repeated on this commit. Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Apply the existing regex engine timeout to the complete declaration grammar. If proving a declaration exceeds 50ms, preserve executable classification so hidden archive members remain visible to SC9. Cover whitespace near-misses across export, interface, namespace, module, and ambient variable declarations through the production classifier, with valid controls and forced-timeout archive checks. Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(report): display terminal report text literally (cherry picked from commit b30ba80) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…space fix(artifacts): prevent disk paths from colliding with archive members (cherry picked from commit 2404212) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…ons-20261015 fix: avoid checksum false positives and preserve archive error origins (cherry picked from commit e8850a8) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(ingest): inspect unverified OMS bundle contents (cherry picked from commit 4ae7c7b) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
fix(sc8): count only the walk's own CPU time against its runtime budget (cherry picked from commit 182a475) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
…on-20261007 fix(artifacts): inspect text behind printable binary prefixes (cherry picked from commit 3a8ee68) Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
Signed-off-by: yashrajbasav <yashrajbasav@nvidia.com>
yashrajp22
marked this pull request as ready for review
October 16, 2026 18:06
| on: | ||
| pull_request: | ||
| branches: ["main"] | ||
| branches: ["main", "yashraj/v2.13.0-base"] |
1 task
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.
Prepare v2.13.0 from v2.12.0 with the selected security fixes, blocking scan fixes, and executable Python support. This PR targets
yashraj/v2.13.0-base, pinned to v2.12.0, so deferred changes already onmainare not included.Included changes
The 54 direct cherry-picks cover 55 selected PRs; #743 is already included in #734. #824 is omitted because the newer default-model rollout in #806 is excluded. Previous defaults remain, apart from the selected NVIDIA Build replacement in #748.
Latest merged fixes included
#725 and #793 were already included. This update adds the final merged changes from #794, #795, #828 and #844.
Integration and release setup
Validation
Current candidate:
e0c568fadb10160bd9f9e1585f90fab345456d3d, compared with previous release headfa44071e63688c8150b053cd24e2be6b8370874d.Candidate wheel:
skillspector-2.13.0-py3-none-any.whl, SHA-25641e097b4127c0aa0732ad31cc909f8aee0b907d97816c866a55b8e782a6ca6dc.Offline checks use isolated Linux containers with Python 3.12.13 and no network or host credentials. These are finite regression checks, not a guarantee of zero defects or a global accuracy claim. Existing incomplete-coverage results are preserved.
Review and publication
Independent source review and a final merge-delta audit covered the exact integrated candidate and found no confirmed blocker or missing requested fix. All six GitHub CI checks passed on
e0c568fa: unit tests, lint, extension tests, DCO, change detection and Docker smoke. Hosted review results remain separate from this local verification.Release authorized after verification. PR #833 was merged as
091a509b3d6ba8ff5be528966e1f43c2e68cf214; its file tree exactly matches tested candidatee0c568fa. GitHub release v2.13.0 is published, and the publication workflow passed. Deferred features and model-default changes remain excluded. Live-provider qualification remains incomplete as detailed above; provider failures are reported as incomplete analysis, and this release does not claim to repair provider latency or invalid responses.Published wheel SHA-256:
41e097b4127c0aa0732ad31cc909f8aee0b907d97816c866a55b8e782a6ca6dc. Published source archive SHA-256:2f4a3200c7fec9b6e79ee76c5d58a54f7709ab82c46750b6a1c96d83606fd6d8. Both match the validated local artifacts exactly.The CI source archive initially included five build-cache metadata files. Its 410 shared files matched the tested archive; the published source asset was replaced with the validated local archive to remove the cache files. A follow-up should move the release build cache outside the checkout or exclude it from source distributions.