mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-25 00:33:49 +00:00
43 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
827d25931b
|
Stop 19 test files racing on one PowerShell startup cache (#9371)
* Stop 19 test files racing on one PowerShell startup cache
Backend CI run 32341628757 on `
|
||
|
|
2043c734c4
|
Take two ubuntu jobs off their own runners: absorb one, delete the other (#9360)
Over 400 completed main push runs, 25 of 96 job types execute in under 120s: 1116s of work spread across 25 runners, each queuing for about three hours. Two of them are dealt with here. Security audit :: pytest tests/security 72s exec, 11096s queue Unsloth export capability :: capability (ubuntu-latest) 67s exec, 10642s queue Different problems, so different treatments. The security suite MOVED, onto the Workflow trigger lint runner ------------------------------------------------------------------------ Same argument that put the lockfile and load-orchestrator lanes into Lint CI in #9176: work with a narrow trigger, moved into a job that was going to occupy a runner on this commit anyway, can only reduce the slots a commit takes. Here the trigger widens too, since this host has no paths filter and security-audit.yml's pull_request does, so the suite now runs on every pull request rather than on the ones that touch its paths. This host and NOT Lint CI, where the other lanes went, and that is the whole decision. Lint CI installs shellcheck from apt, so its harden-runner has to permit escalation and an apt mirror; a security gate moved there would run under a policy weaker than the one it has today. Workflow trigger lint's harden-runner block is byte-for-byte identical to the one the job carried in security-audit.yml (block, disable-sudo, the same six endpoints), so nothing about its isolation changes. harden-runner binds per runner, not per step, which is what makes that the deciding constraint rather than a detail. Folded into the existing pytest invocation rather than added as a step of its own. I wrote it as a separate step first, so a security regression would not be reported as a workflow-guard failure, and test_the_guards_run_in_one_pytest_invocation rejected it: one step per module costs about 15s of interpreter and conftest startup, measured in this repo at 53.9s as one invocation against 300.8s as one each. The guard is right and the attribution preference is not worth 15s. pytest and PyYAML are now pinned here to the versions security-audit.yml pinned them to. That suite runs scripts/lint_workflow_triggers.py as a SUBPROCESS and asserts on its exit semantics, so a pytest or PyYAML that resolves differently changes what it is asserting against. The capability ubuntu leg was DELETED, because it was already duplicated ------------------------------------------------------------------------ That workflow's own comment already explains why it has no macOS leg: every test in tests/test_export_capability.py goes through _patch(), which monkeypatches _has_torch, get_device and is_apple_silicon, so a real Mac proves nothing a Linux runner does not -- and studio-backend-ci.yml runs the same file on ubuntu-latest as part of `pytest tests/`. That argument reaches one step further than it was taken. If Backend CI covers the file on Linux, the ubuntu leg HERE is the duplicate too. Checked: the file is not in that job's --ignore list. The import-safety test does not need a torch-free image either; it installs its own builtins.__import__ blocker and drops preloaded torch/unsloth from sys.modules, so it proves the same thing inside Backend CI's fully installed environment. Windows stays. Nothing else in CI runs that file there, and _has_torch's import probe is the per-OS behaviour the job exists for. The guard ------------------------------------------------------------------------ tests/studio/test_short_job_absorption.py, wired into the unfiltered job. Both changes fail silently rather than loudly if they regress, which is what it is for: - the suite still runs somewhere, and no longer runs twice - the absorbing job's harden-runner has not widened past the six endpoints the suite came with, since "the policy is identical" is the entire justification for this host - the absorbing job has not gained a paths filter - capability still has its Windows leg - studio-backend-ci.yml still runs the whole tests/ tree and does not name test_export_capability.py, because one line added to that --ignore list would remove the coverage the ubuntu leg was deleted for, and nothing would turn red Mutation-tested, each failing exactly one test: drop tests/security from the invocation; add one endpoint to the allowlist; add --ignore=tests/test_export_capability.py to Backend CI; remove the Windows leg. Also corrected a docstring in tests/security/test_scan_packages.py that named tests-security and what it installs. It was about to become false. Verification ------------------------------------------------------------------------ tests/security under the host's own -n 4: 409 passed, 6 skipped. scripts/lint_workflow_triggers.py: OK across 41 workflow files. All three workflows still parse; security-audit.yml keeps its other 4 jobs. Net: 7 short ubuntu slots per commit, down to 5. A note for whoever extends this. The census that found these 15 candidates was partly stale and I nearly acted on it: the lockfile and load-orchestrator rows were already absorbed by #9176 and their samples were pre-merge tails, and the two Local Agent Guides rows show ~0s because they are if-gated to schedule and dispatch, which is a skip and not a fast job. Read the trigger before ranking by duration. Of the remaining candidates, Scorecard is blocked by its job-level id-token: write, the Kaggle gate by a downstream needs:, npm-provenance by an audit egress policy plus registry.npmjs.org, and the notransport clean-install lane by a container that asserts several common tools are absent. |
||
|
|
de102032ee
|
Desktop: ship a complete Linux AppImage (#9113)
--------- Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com> |
||
|
|
567b3b7f99
|
Allowlist huggingface-hub's http_backoff loop so Security audit stops failing (#9252)
* Allowlist huggingface-hub's http_backoff loop so Security audit stops failing Security audit has been red on every main commit since |
||
|
|
659b9dad3f
|
Security audit: pin openai, and re-review the four digest-pinned entries (#9148)
* Security audit: pin openai, and re-review the four digest-pinned entries
The extras shard of the security audit has been red on main since 2026-08-17
19:14Z with four non-baselined CRITICALs, all in `openai`:
Harvests environment variables/secrets AND makes network calls
openai/_client.py, openai/lib/azure.py, openai/lib/bedrock.py
Accesses cloud metadata/IMDS AND makes network calls
openai/auth/_workload.py
It is not a code change here. The last green run was 18:23Z and every run after
18:40Z failed on the same four findings, on main and on unrelated PR branches
alike, which is upstream drift rather than anything in the tree.
`openai>=2.7.2` is the only floating spec in extras.txt that carries digest-pinned
baseline entries. openai 3.2.0 published at 19:14Z and changed all four files.
Confirmed by digest: the four `file_sha256` values in the baseline match openai
3.0.0 and 3.1.0 exactly and none of 3.2.0's.
The pin is deliberate and correct (#8104, #8565): the evidence for these entries
records that a network call exists but not where it goes, so `client.post(...,
data=api_key)` appended to one of these files would leave the evidence hash
untouched. Only the file digest can reopen that, so any edit upstream makes must
red the gate until someone re-reads it.
Which is the review, done here. Every destination in the four files at 3.2.0 is
first-party or a documented cloud metadata endpoint: api.openai.com,
auth.openai.com, 169.254.169.254 (Azure IMDS), metadata.google.internal,
management.azure.com, and bedrock-mantle.{region}.api.aws. The one instance-level
POST is the workload-identity token exchange, which defaults to
https://auth.openai.com/oauth/token and sets follow_redirects=False. The env reads
are OPENAI_API_KEY, OPENAI_ADMIN_KEY, OPENAI_WEBHOOK_SECRET,
AZURE_OPENAI_API_KEY, AZURE_OPENAI_AD_TOKEN and AWS_BEARER_TOKEN_BEDROCK, each
used to authenticate to its own service. Same benign patterns as the reviewed
3.0.0, so the four entries are re-pinned to 3.2.0's digests and nothing else in
the 214-entry baseline is touched.
Re-baselining alone would only buy time until the next release, so the spec is
pinned too. Every other requirement in extras.txt is already an exact pin (#8408
pinned the bare ones and skipped this one because it had a specifier). Pinning
makes the bump deliberate: whoever raises the version is the one who re-runs
--write-baseline and re-reads the diff, instead of the gate going red on release
day for a change nobody here made.
The new test asserts that invariant directly, since it is the part that will be
got wrong again: a package with digest-pinned baseline entries must not float in
studio/backend/requirements. Reverting the spec to `openai>=2.7.2` fails it and
names extras.txt:62.
Verified:
- `scan_packages.py 'openai==3.2.0'` exits 0; on 3.1.0 it exits 1, so the pin
still bites and the entries were not widened.
- The whole extras shard (`--with-deps -r extras.txt`, 128 archives) exits 0
with 0 active CRITICAL/HIGH, against 4 CRITICAL and exit 1 before.
- tests/security/test_scan_packages.py: 121 passed.
Unrelated, and not a defect: the `openai-whisper==20250625` line in the same log
is an INFO, not the failure. That version is on PyPI as an sdist only, and the
bulk resolve runs `--only-binary :all:` so it never executes a setup.py, which is
the scanner's whole security model. "from versions: none" is what pip says about
an sdist-only package under that flag, and the per-spec fallback plus the direct
sdist fetch that follow are the designed path working. The scipy
`assert_raises(ValueError, CubicSpline, ...)` evidence quoted alongside it is a
MEDIUM and cannot fail the gate; it is a false positive on `bc1`, a boundary
condition variable in scipy's own test file that looks like a bech32 address.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Keep the openai pin off Python 3.9, which the first version of it broke
`openai==3.2.0` on its own was a regression, caught simulating the install rather
than reading it. openai 3.x is `requires-python >=3.10` and this project is
`>=3.9,<3.15`, so an exact pin resolves to nothing at all on 3.9, where `>=2.7.2`
had quietly been selecting 2.48.0. Fixing a CI gate by breaking an install is not
a fix.
Split on the marker instead, which is what the rest of this file already does for
soundfile and tabulate. 2.48.0 is the newest release that accepts 3.9, so the 3.9
branch pins exactly what 3.9 was resolving to before.
Resolved with uv against every interpreter in requires-python, --no-deps, three
ways (main, the bad pin, this):
spec 3.9 3.10 3.12 3.13 3.14
>=2.7.2 2.48.0 3.2.0 3.2.0 3.2.0 3.2.0
==3.2.0 NO SOLUTION 3.2.0 3.2.0 3.2.0 3.2.0
split 2.48.0 3.2.0 3.2.0 3.2.0 3.2.0
So this is behaviour-preserving: identical to main on every supported Python, and
the whole of extras.txt resolves to the same 31 packages on 3.10, 3.12, 3.13 and
3.14 before and after, differing only in the `# via -r` filename. 3.9 fails on
both sides for an unrelated and pre-existing reason (scikit-learn==1.7.1 is itself
>=3.10), so nothing here changes that either.
The security audit is unaffected: it runs on 3.12, markers are evaluated by pip,
and the scan of the two-line spec fetches only 3.2.0 and exits 0. The 3.9 branch
is never scanned, exactly as the floating spec's 3.9 resolution never was.
The guard is widened to the second half of the same invariant: the exact pins for
a digest-pinned package must cover every supported Python between them, so a
marker partition with a hole in it fails. Its limit is now written down rather
than implied. It is marker-only and offline, so it catches a `>= "3.11"` beside a
`< "3.10"`, but it cannot catch a single unmarked pin whose release does not
support 3.9, because that means asking PyPI for the release's requires-python.
The resolution simulation above is what covers that case, and the test module
stays network-free.
Verified: reverting to `openai>=2.7.2` still fails the guard naming extras.txt.
tests/security/test_scan_packages.py: 121 passed.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Widen the pin guard to the pyproject inputs, and make `==` mean one version
Two review findings, both real, both reproduced before fixing.
The guard read `studio/backend/requirements/*.txt` and stopped there, but
security-audit.yml builds `audit-reqs/unsloth-deps.txt` out of pyproject's
`project.dependencies` plus the `huggingfacenotorch` extra and feeds that to the
hf-stack shard. A digest-pinned package declared there was invisible to the guard,
which is the whole failure mode it exists to catch, one file over. It now reads
both halves, from one helper that mirrors the workflow's own input transform.
Reading pyproject turns up the counterexample the finding named: unsloth_zoo is
digest-pinned (hf_xet_health.py, the credential send that motivated #8104) and
declared `unsloth_zoo>=2026.8.12`. That one is deliberately not version-pinned and
must not be. The recurrence being prevented is an upstream release WE DO NOT
CONTROL changing the bytes and reddening main on a day nobody touched the repo;
unsloth_zoo is ours, released in lockstep, and an exact pin would break that. When
its digest reopens, the change is one of ours and re-reviewing it is the point of
the pin. So it is named in an exemption rather than quietly skipped, and the
exemption is itself asserted: it must contain only names that really are digest
pinned, and at least one third-party package must remain, so the guard cannot be
defused by growing the list. That assertion earned its keep immediately, failing
on `unsloth` until it was removed for having no pinned entry.
Second, `==` has to mean one version. The old prefix regex accepted `openai==3.*`,
which pip resolves as a prefix match to whatever 3.x is newest, recreating the
exact release-day failure. The specifier is now parsed with
packaging.requirements rather than pattern-matched: exactly one clause, operator
`==`, version not ending in `.*`. Parsing also drops the hand-rolled name and
marker splitting, so extras and whitespace stop being edge cases.
Four negative controls, each red with its own message and green after:
- `openai>=2.7.2` in extras.txt: not pinned to one version, names extras.txt:68.
- `openai==3.*`: same assertion, names the wildcard spec.
- `< "3.9"` instead of `< "3.10"`: openai uncovered on 3.9.
- `openai>=2.7.2` added to pyproject's dependencies: names pyproject.toml, which
is the finding above and was green before this commit.
tests/security/test_scan_packages.py: 121 passed.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Read pyproject through tomli when tomllib is not there
pyproject sets requires-python >=3.9 and testpaths ["tests/security"], so a
bare pytest from the repo root collects this module on 3.9 and 3.10, where
tomllib does not exist (it landed in 3.11, PEP 680). The two helpers added
here imported it unguarded, so on those interpreters the file raised
ModuleNotFoundError instead of reporting a result. tomli is already pinned for
python_version < 3.11 in extras-no-deps.txt, and four other suites in this repo
already use exactly this fallback.
The regression test supplies the backport rather than requiring it. The
tests-security job installs only pytest and PyYAML, so a test that leaned on a
real tomli being importable would importorskip its way to green there and never
execute the branch at all. Registering the parser this interpreter does have
under the name the fallback looks for keeps it load-bearing everywhere, while
import tomllib is made to fail for the duration so the fallback is provably
what gets consulted. Checked red on the unfixed helpers and green on the fixed
ones, including with tomli hidden.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
||
|
|
47dcad27c0
|
Scan package archives across cores instead of one at a time (#9024)
* Scan package archives across cores instead of one at a time
The three pip scan-packages shards cost 21.7 runner-minutes per push
(studio 10.17, extras 6.63, hf-stack 4.88), the bulk of Security audit.
Almost none of that is network. Timing the hf-stack shard locally:
Scanning 49 package(s) (with transitive deps)...
0.0s
Downloaded 110 archive(s).
9.7s
Summary
315.6s
pip download is 9.7s of 315.6s. The rest is the serial loop calling
scan_archive on each archive, which is pure CPU: regex over decompressed
archive members. Every archive is independent.
Pooled at 4 workers the same shard runs in 97.7s, and the report is
byte-identical to the serial one. imap with chunksize=1 yields in
submission order so findings are assembled exactly as before; chunksize=1
is also what makes next(timeout=) available at all, since above 1 CPython
returns a bare generator with no timeout support.
The archive-limit [WARN] lines are captured from the worker and replayed
in task order rather than landing wherever a worker reached them, so two
runs of the same input produce identical logs.
Default is min(4, cpu count), matching the runner, with --jobs to override
and --jobs 1 to force the old path. No workflow change needed.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Exit 2, not 1, when the scan pool stalls
raise SystemExit(<string>) prints the string and exits 1, per the language
reference: "if it has another type (such as a string), the object's value is
printed and the exit status is one".
1 already means "non-baselined CRITICAL or HIGH findings detected" in this
scanner's documented contract, and 2 means an incomplete scan. So a dead worker
reported an infrastructure failure as a detected threat, and skipped the SCAN
INCOMPLETE block that tells the operator coverage was lost.
The stall is now recorded beside the pip-download failures and reported by that
same block, so it exits 2 and says why. The header counts both kinds, so it no
longer calls a scan stall a "pip download failure".
Regression test drives a pool whose first next() raises TimeoutError and asserts
exit 2 plus the report. Reverting to SystemExit fails it.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
||
|
|
135147814b
|
Fix CI on main: stale test doubles, a stale router stub, and two source defects (#8956)
* Fix CI on main: stale test doubles, a stale router stub, and two source defects
main has been red since Aug 14 and every open PR inherits it. Five clusters, none
of them caused by the PRs that were showing them.
context_length (13 tests, plus 4 more in disguise). #8700 added an unguarded
llama_backend.context_length read to the chat-completions path and updated five
test files, missing three. The real LlamaCppBackend has had the property for a
long time, so no user was ever affected; the doubles were simply
under-specified. The four gguf_stream_slot_release failures are the SAME bug:
those doubles reach the same line, but the AttributeError is swallowed into the
response task and surfaces as a 20 second timeout, which reads as a flake.
Nine hand-written doubles across five files each re-declared the same attribute
block with no shared base, so one new read broke whichever files happened not to
be updated. They now share FakeLlamaCppBackend, and a canary drives the real
route with a bare double so the next such read fails in one place, named, at the
point of the change. The stream waits no longer discard the driving task's
exception, so that class of failure cannot present as a bare timeout again.
youtube_router (2 tests). routes/__init__.py exports it and main.py imports it;
the app is fine. test_desktop_auth stubs sys.modules[routes] with a hardcoded
list of 17 routers, deliberately, to avoid importing the ML stack. #8648 added a
router and did not update it, the second time this has happened after
openai_codex_auth_router in #8511. The stub is now derived from main.py's own
import block, so it cannot go stale.
Repo tests (CPU), 6 failures, of which two are real source defects:
llama-extra-args.ts put the Studio brand into a user-visible validation message,
which the desktop branding contract forbids in runtime surfaces, and
test_playwright_server_lifecycle.py read checked-in files without an encoding,
which is a real Windows cp1252 crash the lint exists to catch. Both fixed in the
source. The other four are stale assertions chasing text that #8702 legitimately
moved or reflowed; they now assert the behaviour instead, via the real
override_lookup_candidates() and the element-scan pattern their own siblings
already use.
Not addressed here: pip scan-packages :: hf-stack reports 173 findings in
third-party deps under SCAN_ENFORCE=1 and is red on main too. Baselining a
supply-chain scanner to get green is the wrong reflex, so it wants its own look.
* Studio: break the settings/chat import cycle that stopped the UI rendering
#8932 added SIDEBAR_ORGANIZATION_STORAGE_KEY to the @/features/chat barrel and
had general-tab.tsx read it back out of that barrel. The key is used at MODULE
scope, in the storage-key list, and the barrel is part of an import cycle that
reaches this file, so the binding is still in its temporal dead zone when the
list is built:
Cannot access 'SIDEBAR_ORGANIZATION_STORAGE_KEY' before initialization
That kills the whole module graph, so the page renders nothing. It is why
Frontend CI has been failing on main with a Playwright locator that finds no
elements, which reads as a flaky browser test rather than a module-init error.
Importing the key straight from its module breaks the cycle. Verified by
bisection with the real browser smoke: it passes at
|
||
|
|
83e163f13c
|
Revert "Desktop: ship a complete Linux AppImage (#8695)" (#8823)
This reverts commit
|
||
|
|
93a77d4104
|
Desktop: ship a complete Linux AppImage (#8695)
* Desktop: ship a complete Linux AppImage * Address complete AppImage review findings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Isolate AppImage GIO environment * Fix release resolver lane count for PR #8695 * Keep the AppImage runtime out of host launcher processes * Ship a resolvable AppImage .DirIcon * Scrub AppImage GUI paths from managed children --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> |
||
|
|
90a6a236b5
|
Harden the workflow-trigger lint: scan .yaml, and host it outside the workflow it audits (#8545)
* lint_workflow_triggers: scan .yaml workflows too, and pin security-audit to every PR GitHub Actions loads both .yml and .yaml out of .github/workflows/, but the trigger lint only globbed *.yml, so an evil.yaml carrying pull_request_target would run for real and still lint clean. Also adds a regression test asserting security-audit.yml's pull_request trigger has no paths filter, since the lint job lives inside that workflow. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Host the workflow-trigger lint in its own unfiltered workflow The lint job lived inside security-audit.yml and inherited its triggers, so the gate was only ever as broad as an unrelated heavy audit workflow's filter policy. Move it to workflow-trigger-lint.yml, which triggers on pull_request with no paths filter. pull_request resolves the workflow file from the PR merge ref, so a PR that adds a paths or paths-ignore filter to the host skips the host for its own PR and the gate never reviews the change. A pytest assertion cannot catch that, because the test runs inside the workflow being skipped. The lint now checks its own host instead: it rejects both filter keys on any workflow that runs the script, and fails when no unfiltered host exists at all. That still leaves the tampering PR itself, so CODEOWNERS now covers .github/workflows/ and CODEOWNERS itself, which is the merge-time control. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Detect the lint host from parsed steps, and check effective CODEOWNERS Host detection matched a regex against the raw workflow text, so a commented out '# - run: python3 scripts/lint_workflow_triggers.py' registered as a host. That defeats the fail-closed check: delete the real workflow, leave a comment behind, and --require-host still passes. Read the parsed jobs/steps instead, which also covers multi-line run blocks. The CODEOWNERS guard asserted a matching rule existed somewhere in the file. GitHub applies only the LAST matching pattern, so appending '* @someone-else' would take over while the guard stayed green. Resolve the effective owners for a workflow path and for CODEOWNERS itself, and add a test that the guard fails when a broader rule is appended. * Require the lint host to be unnarrowed and blocking, and check every workflow's owner The host check only rejected paths and paths-ignore. branches, branches-ignore and types skip PRs just as effectively, so a host restricted to another branch counted as unfiltered. Require a bare pull_request: with no configuration at all, which covers those keys and any future one. A host whose lint step or job carries continue-on-error runs but cannot fail, so --require-host passed while findings were advisory. Reject that too. The CODEOWNERS guard probed only the lint host and CODEOWNERS itself, so a narrower trailing rule could take a different workflow away from its owner while both probes still passed. Check every workflow file has some effective owner, keeping the danielhanchen requirement for the two that matter most. Delegating a workflow to another maintainer stays fine; leaving one unowned does not. The rule parser also skipped ownerless lines, which are valid CODEOWNERS and clear ownership, so a bare pattern was an invisible carve-out. * Reject an if-gated lint host, and glob CODEOWNERS directory patterns An if: condition on the lint step or its job skips the lint while the run still succeeds, which is the same defeat as continue-on-error by another key. Reject any if: on a host rather than trying to prove one always true. The CODEOWNERS matcher compared trailing-slash patterns as literal substrings, so a valid trailing rule like '**/workflows/ @someone-else' took the lint host away from its owner while the guard still computed danielhanchen. Match every directory prefix with fnmatch, allowing unanchored patterns to start at any depth, and cover globbed, unanchored and wildcard-segment rules in the regression test. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Require a real lint invocation, reject needs:, and tighten owner matching Host detection accepted any run text containing the script name, so 'echo scripts/lint_workflow_triggers.py' counted as running the gate. Require the script to appear as the argument of a python interpreter at the start of a command. A lint job with needs: is skipped when its prerequisite is skipped, and the workflow still succeeds. Reject needs: on a host, same as if: and continue-on-error. CODEOWNERS wildmatch lets '**/' match zero directories, so '/.github/**/workflows/' overrides the host while fnmatch returned false. Expand '**/' into both forms before matching. Owner tokens are also validated: GitHub cannot request review from a bare word, so a trailing rule naming 'not-an-owner' leaves the path effectively unowned and no longer counts. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Reject a defanged lint invocation, and match CODEOWNERS globs per segment A host running 'lint_workflow_triggers.py || true' passed every check: it is a real invocation, the trigger is unfiltered, and no metadata flags it, yet findings can never fail the run. Same for a command passing --workflows-dir at somewhere empty, or --no-require-host, which leaves the job green while it gates nothing. Reject failure-masking shell and both neutering flags. The CODEOWNERS matcher used fnmatch, whose '*' consumes '/'. That made '/.github/*' appear to claim nested workflow files, so a valid CODEOWNERS change touching only direct children of .github would have failed the guard. Translate patterns to a regex instead, where '*' stops at a separator, '**' crosses them and '**/' may match zero directories, and only expand directory prefixes for patterns without a wildcard. This replaces the '**/' expansion workaround, so the helper is now wrong in neither direction. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Require python to execute the lint file, and anchor slashed CODEOWNERS patterns 'python3 -c pass scripts/lint_workflow_triggers.py' names the script but runs the -c program and exits 0. Confirmed directly: rc=0, nothing scanned. The old regex allowed arbitrary arguments before the basename, so that counted as a host. Tokenize the command instead and require the script to be python's executed file, walking option flags but stopping at -c or -m. CODEOWNERS anchoring followed gitignore only for a leading slash. A pattern with an internal separator is root-relative too, so 'workflows/lint.yml' was being tried at every depth and appeared to override '.github/workflows/lint.yml'. That failed valid CODEOWNERS changes aimed at a top-level workflows/ directory. A bare 'workflows/' still floats to any depth. Also pins that ordinary invocations keep working: -u, -X with a value, and plain 'python'. * Require the host to run the repo lint as a plain command with no arguments Three separate holes had one shape, so this replaces the growing list of special cases with a single rule: a host runs scripts/lint_workflow_triggers.py as a standalone command, with no arguments. That covers a decoy /tmp/lint_workflow_triggers.py sharing the basename; a pipeline or background job, where the step's exit status need not be the lint's, since the default run: shell is bash -e with no pipefail; and every argument, including --help, which exits 0 before scanning, and the abbreviated --workflows-d that slipped past the old substring denylist. The _MASKED regex and the NEUTERING_FLAGS list are both gone. argparse also now runs with allow_abbrev=False, so --workflows-d is rejected by the script itself rather than only by the host check. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Pin the lint path exactly, require a lone command, and check the step shell Three more ways a host could look wired while running nothing. The path was a suffix match, so a decoy at /tmp/scripts/lint_workflow_triggers.py passed. Require the exact repo-relative token, or the ./ form. Judging lines in isolation cannot tell a call from a definition, so an invocation parked in an uncalled function or a here-document read as enforcing. The step body must now be the lint command and nothing else, which sidesteps shell parsing entirely and makes the separate set +e check redundant. A custom shell template such as bash -c '"{0}" || true' wraps the command and drops its exit status while the run line stays plain. Only bash and sh count, resolved through step, job defaults.run and workflow defaults.run. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Classify publishers by stem, and tighten what counts as running the lint Scanning .yaml made PUBLISH_WORKFLOW_NAMES inconsistent: a rename to release-desktop.yaml would be loaded but no longer classified as a publisher, so a cache key shared with a PR workflow stopped being a finding. Match on the stem instead. This one was introduced by the .yaml change in this PR. Three ways the interpreter was accepted without running the file. The regex matched any command containing python, so /tmp/fakepython passed; require the BASENAME to be a python, keeping any directory prefix. -V, --version, -h and --help before the path make python print and exit 0, confirmed directly, so they now disqualify like -c and -m already did. And working-directory resolves the same plain command to a different file, so a host setting it on the step, the job defaults or the workflow defaults is rejected, alongside the existing shell check. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Allowlist interpreter flags, reject redirecting env, validate the pull_request value Interpreter flags are now an allowlist rather than a denylist. Each round of review found another flag that stops the file running or masks its status, and -i was the latest: it enters the REPL after the script, so on EOF the process exits 0 even though the lint called sys.exit(1). Confirmed directly, rc=0. Only flags that leave run-this-file-and-return-its-status intact are accepted, so an unrecognised flag fails closed instead of needing to be enumerated. BASH_ENV, ENV and PATH redirect the step without the command text changing at all: non-interactive bash sources BASH_ENV before the step script, so an exit 0 there ends the step before the lint runs. Rejected at workflow, job and step scope. pull_request: false was read as unrestricted, because only mappings were inspected. GitHub rejects a non-mapping event configuration and will not load the workflow, so the value must now be bare or a mapping. * Tighten comments on the workflow-trigger lint Comment and docstring wording only, no behaviour change. The blocks grew a clause per review round; this keeps the reason each rule exists and drops the retelling. * Stop trusting PR-controlled interpreters, option values and startup vars Three variants of one mistake: the host check trusted content the PR itself can supply. A basename-only interpreter check accepted ./python3, which a PR can add to the repository root. Require a bare command or an absolute system path. An allowlisted value-taking option had its value consumed unchecked, so a command substitution in it would run before python started. Option values are now rejected for shell syntax like trailing arguments already were. PYTHONPATH, PYTHONHOME and PYTHONSTARTUP join BASH_ENV, ENV and PATH: a sitecustomize.py on PYTHONPATH is imported before the script and can exit 0. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Reject expansions in the interpreter token and containerized host jobs The trusted-path check read the token before bash expands it, so "/usr/$(...)/python3" passed on basename and prefix while the substitution ran first. Reject shell syntax there, as option values and trailing arguments already were. A job with container: runs its steps in a PR-selected image that controls the shell and environment, which the workflow-level env merge cannot see. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
4a53e80118
|
security: the network check could not see httpx2 (#8565)
* security: the network check could not see httpx2 httpx2 is the pydantic-maintained successor to httpx and a separate import name, so the httpx-only alternative in RE_NETWORK did not match it. openai 3.0.0 requires httpx2 and routes every call through it, which made the SDK's own HTTP invisible to every combined check that needs a network half: secrets plus network, IMDS plus network, archive plus network. The visible symptom was openai/auth/_workload.py reporting as a standalone HIGH "accesses cloud metadata" rather than the combined CRITICAL, because the scanner could not correlate the IMDS URLs with the httpx2.Client() calls beside them. Widening it surfaces four findings, all in openai, all reviewed and benign, all baselined here. Two tests pin the behaviour so the widening cannot be reverted silently. * security: pin the openai baseline entries to the reviewed file digest RE_NETWORK matches httpx2.Client where it appears in a signature, but not a call through an instance. So a client.post(..., data=api_key) appended to one of these files contributes no evidence: the evidence hash is unchanged and the entry keeps suppressing it. Reproduced, the two hashes are byte-identical. All four entries now pin the reviewed digest, the mechanism _load_baseline already documents for files whose danger sits outside the matched lines. _workload.py is pinned too: it has real httpx2.Client() calls, but an added client.post would leave its evidence unchanged just the same. Five older entries for these paths are dropped. They were baselined when openai used bare httpx and carry the identical annotation-only evidence, inert against 3.0.0 but suppressing on any resolve back to an httpx-based version. They cannot be pinned to an artifact that was never reviewed, so they go and reopen if that evidence recurs. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
f5c64f4a08
|
security: lockfile audit must block non-registry sources and missing integrity by default (#8541)
* security: lockfile audit must block non-registry sources and missing integrity by default The pre-install lockfile gate detects non-registry npm resolved URLs, missing npm integrity hashes, non-registry Cargo sources and missing Cargo checksums, but #5604 left all four out of BLOCKING_KINDS, so they only printed a :⚠️: and the script exited 0. Every workflow calls the auditor without --strict immediately before `npm ci`, which runs package lifecycle scripts, so a lockfile pointing at an attacker controlled tarball with a matching integrity value passed the gate and its postinstall ran on the runner. Move those four kinds into BLOCKING_KINDS. An integrity hash written by whoever also wrote the resolved URL proves nothing; the registry origin is what makes it meaningful, so both belong on the blocking side of a pre-install fetch gate. Kinds that describe an incomplete entry rather than a fetchable source (missing-resolved-url, unsupported-lockfile-version) stay advisory, and --strict still escalates everything. All four checked-in lockfiles pass in default mode and under --strict, so no existing job changes colour. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * lockfile audit: correct the --strict help text for the new default blockers The --strict help still told operators that default mode blocks only known-malicious versions, IOC strings and broken lockfiles and that everything else exits 0. Provenance and integrity findings now block by default, so --help contradicted BLOCKING_KINDS for anyone deciding whether they needed --strict. * lockfile audit: tighten the comments added by this change * lockfile audit tests: tighten docstrings --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
eeb958dd1b
|
release-desktop: pin trusted-signing-cli by digest instead of trusting a cache (#8417)
* release-desktop: pin trusted-signing-cli by digest instead of trusting a cache The Windows release path restored `~/.cargo/bin/trusted-signing-cli.exe` from actions/cache and skipped the pinned `cargo install` on a hit. The restored executable then went on PATH and was invoked by the signing script, in steps that hold the Azure Trusted Signing credentials and the Tauri signing key, with `trusted-signing-cli --version` as the only check. Any binary can print a version string, so a poisoned or stale cache entry would have signed a release. A cache is not an integrity mechanism. Upstream publishes the same tool as a prebuilt release asset, so pin the URL and gate it on SHA-256, the same shape as the AppImage toolchain pinned a few steps below: the URL is reproducibility, the digest is the integrity control, and a mismatch fails the release instead of signing with an unknown binary. A 7 MB download is also faster than the cache restore it replaces and much faster than the 240s source build. The install directory is prepended to PATH rather than `~/.cargo/bin`, since swatinem/rust-cache restores that directory and it can carry its own unverified copy under the same name. The verify step now asserts that PATH resolves to the digest-checked file, because the signing script calls the tool by bare name. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * release-desktop: name the shell the signing install needs, and simulate the steps Two follow-ups from validating the pin on a real windows-latest runner. `Invoke-WebRequest -MaximumRetryCount / -RetryIntervalSec` are PowerShell 6+ only. windows-latest defaults to pwsh so the step works, but it was inheriting that from a runner default while every other Windows step in this file names `shell: pwsh` outright. Under Windows PowerShell 5.1 the body fails with "A parameter cannot be found that matches parameter name 'MaximumRetryCount'", confirmed on a runner. Both steps now declare the shell. The new simulation test runs the two step bodies instead of only reading them. It extracts them from the workflow, wraps them the way the runner does (prepend `$ErrorActionPreference = 'stop'`, append the $LASTEXITCODE propagation, invoke with `pwsh -command ". '<file>'"`) and serves the release asset from a local HTTP server, so a tampered, truncated, withdrawn or unreachable download is deterministic and offline. It covers the happy path, idempotent re-runs, paths with spaces, a case-different digest, an empty pin, and every failure mode of the verify step including an unverified copy winning PATH. Skipped without pwsh; the real asset is fetched only when UNSLOTH_NETWORK_TESTS is set. * tests: make the Windows verify fixtures real instead of vacuous Three defects in the simulation file, all found in review. The cannot-start test never reached the try/catch it names. It planted an extensionless file, so on Linux the identity check rejected it first and on Windows it was not a runnable .exe; the assertion accepted any ::error::, so it passed on the wrong failure and would have kept passing if the catch block regressed. It now plants a non-loadable trusted-signing-cli.exe at the verified path, asserts the "could not be started" annotation, and is marked needs_pathext since only PATHEXT can resolve a bare name onto it. The two Windows-only verify tests could not have passed when enabled: their fixtures were POSIX shebang scripts rather than the .exe both PATHEXT and the identity check require, and PATH was joined with ':'. They now borrow real executables, the running interpreter for the exits-zero case and where.exe for the exits-non-zero case, and every PATH is joined with os.pathsep. The bare-name contract test only reads a file in the tree but sat behind the network marker, so the default job never enforced that the signing script still invokes the tool by the name whose resolution the other tests check. Marker dropped. * tests: tighten the signing test comments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
e8d0d39b08
|
Scan Windows bundles with MpCmdRun when the Defender cmdlets are down (#8358)
* Scan Windows bundles with MpCmdRun when the Defender cmdlets are down * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Guard each Defender config check on the cmdlet that supplies it * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten the comments this branch added * Treat a control sample quarantined mid-scan as the control firing * Tighten the comments added by the review rounds --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
08dfbdf45a
|
Make publish-desktop-updater.yml manual-dispatch only (#8356)
* Make publish-desktop-updater.yml manual-dispatch only * Gate the legacy bridge on the desktop-bundle check it depends on * Tighten the comments this branch added * Validate the target release before deleting its signature assets * Refuse to carry a manifest onto a draft or prerelease target * Refuse a repair target whose release state cannot be read * Tighten the comments added over this PR's review rounds |
||
|
|
de12348a66
|
Stop writing the desktop build provenance section onto the release (#8340) | ||
|
|
3bbed688a8
|
CI: skip Windows release cache saves (#8325) | ||
|
|
67af9aa825
|
Desktop: unify normal release updater flow (#8298)
* CI: point desktop updater test at fork * Studio: simplify desktop setup progress UI * CI: limit updater A/B build to macOS and Windows * CI: publish macOS and Windows updater test assets * CI: build Linux and Windows updater test assets * CI: pin updater test to Windows 2022 * Fix latest-main updater test workflow merge * Resolve latest-main startup message merge * Desktop: unify normal release updater flow * Desktop: validate updater signatures * Desktop: preserve generated updater signature * Restore macOS and target the existing v release for PR #8298 Restores the macOS leg that was dropped from the release pipeline: the macos-latest matrix entry, the .dmg and .app.tar.gz assets, the darwin-aarch64 platform entries in latest.json, and darwin-aarch64 in the required families of both release-desktop.yml and publish-desktop-updater.yml. Without them no macOS bundle is published and macOS clients find no matching platform in the manifest, so they stop updating entirely. Targets the v{version} release that already exists instead of creating it. The tag is cut when main is tagged, before this workflow is dispatched, so the old "tag already exists" guard failed every run on this repository; it only passed on a fork where the tags were absent. The guard now requires the release to exist and refuses only when it already carries desktop assets, naming the delete-asset commands to recover a failed publish. Provenance is appended to the release body rather than replacing it, since that body is the changelog. Windows step conditions follow the restored windows-latest matrix entry. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address the review on PR #8298 Gate the asset and manifest uploads on the draft input. The target release is already public, so a validation-only run was publishing unapproved binaries and latest.json to it. Move the provenance edit after the uploads and replace any earlier section instead of skipping it, so a retry that follows a partial upload records the digests that actually shipped rather than the previous build's. Reject a prerelease target in both guards. GitHub cannot mark a prerelease latest, so catching it only at promotion left the bundles already public. Re-read GitHub latest immediately before promotion. The downgrade check runs before a build that can take an hour, and promoting past a newer release would hand every client an older manifest. Build from the release tag rather than the dispatch ref. The release is published before the workflow runs and main keeps moving, so the bundles could come from unrelated source and provenance could record a SHA that is not the tag's. Skip the updater validation when the release carries no latest.json. The v release is published before the bundles land, so the release event fired first and failed on every release; it now fails closed only when the release cannot be read. Scope the signature sweep to the desktop bundles, since the release is shared. Add the AGPL-3.0 header to the two new files, in the style the repository uses. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep updater discovery on desktop metadata and record the built commit for PR #8298 * Send make_latest as the documented string for PR #8298 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Repair the release-creation tests and the publish-side guard for PR #8298 * Fail closed on promotion, order numbered prereleases and keep the pointer forwardable for PR #8298 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolve the newest desktop release lazily and record the updater pointer gap for PR #8298 --------- Co-authored-by: danielhanchen <danielhanchen@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
0cd73cf3fb
|
Make the desktop release contract tests fail when the contract breaks (#8228)
* Realign the desktop release tests with the post-publish VirusTotal job * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tie the release contract tests to what they are meant to guard Checkpoint of in-progress work, mutation testing still outstanding. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Scope the release wait and scan assertions to the mechanism they guard The wait checks searched the whole step, so a one-shot jobs API read beside an unrelated loop passed; they now run against the poll loop body and also require a break. The scan job's condition was accepted on a success() substring, which let a disjunction through; it now has to require success() conjunctively. And the scan's directory was compared by leaf between two download steps, so moving both under a new parent, or repointing the script argument, scanned an empty directory and still reported clean; the argument the step passes is now compared against the download path. * Tighten the comments on the release contract tests * Reject any job-level condition on the scan and require a live poll loop Accepting a condition that merely opened with success() let success() && false through, which skips the sweep after a publication that succeeded, so no job-level if: is accepted at all now: reaching virustotal-scan is needs:'s decision alone. The wait helper likewise selected a loop by shape, so turning while :; do into while false; do kept every assertion green while the shell skipped the API reads and fell through to a download that races the matrix; the helper now only selects an unconditional poll. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
4bd9a2b8c3
|
Format the release permissions test to the kwarg-spacing hook (#8245)
The rewrite in #8240 left two statements wrapped the way I typed them rather than the way scripts/run_ruff_format.py wants them. pre-commit.ci runs that hook over the whole tree on each PR's merge result, so every open PR inherits the failure the moment it merges main. Formatting only; the file's 79 tests pass unchanged. Co-authored-by: danielhanchen <unslothshared@gmail.com> |
||
|
|
b844e55d44
|
Repo tests: pin the post-publish VirusTotal contract instead of the old pre-flight one (#8240)
Some checks are pending
Backend CI / Repo tests (CPU) (push) Waiting to run
Unsloth export capability / capability (ubuntu-latest) (push) Waiting to run
Unsloth export capability / capability (windows-latest) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Unsloth GGUF CI / JSON, images (push) Waiting to run
Unsloth load-orchestrator CI / test (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Mac Studio GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI + API + Update CI / Chat UI, API and Update Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Unsloth Tauri CI / Rust unit tests (windows) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
The desktop release workflow no longer runs a VirusTotal step inside publish-release. #8194 moved it into its own virustotal-scan job that runs after the release is published, and #8193 replaced publish-release's `needs: build` with a "Wait for the build matrix" step so the job can queue for its runner during the build. Five tests still encoded the old shape and fail on main for every PR: TestWorkflowOrdering::test_scan_runs_after_the_release_is_validated TestWorkflowOrdering::test_release_creation_is_deferred_until_after_the_scan TestWorkflowOrdering::test_scan_runs_before_the_assets_are_published TestWorkflowOrdering::test_the_scan_script_is_checked_out_first test_build_matrix_hands_off_assets_without_release_credentials Rewrite them against the current layout rather than dropping the assertions. The scan is a post-publish sweep and is advisory by design, so what is worth pinning is that it cannot be quietly lost: - virustotal-scan exists and `needs: [publish-release]`, so deleting the job or the dependency is red, - the job carries no `if:`, and no step does either except the summary, which is `if: always()` so the verdict survives a failed scan, - the sparse checkout of scripts/virustotal_scan.py is asserted by mechanism rather than by step name, along with the guard that exits 1 when the script is absent, - the scan step does not swallow the script's exit status: no continue-on-error, no `|| true`, no `exit 0`, and no `--fail-threshold` pinned to a value the script treats as never-fail, - continue-on-error appears on exactly one job and on no step of any job that handles a bundle, - the downloaded artifact pattern matches what the build matrix uploads, - publish-release does not re-inline the scan. For the permissions test, the build-to-publish handoff is now gated by the waiter, so assert that instead of `needs: build`: it covers every matrix leg by name, refuses to publish a leg that did not succeed, and refuses to publish a leg whose job record never appeared. Add a check that the new scan job holds no release credentials, since it handles the bundles and uploads them off-box. Docstrings say plainly that this is a post-publish sweep, so a future reader does not go looking for a pre-publish gate that is not there. Co-authored-by: danielhanchen <unslothshared@gmail.com> |
||
|
|
38dec58e08
|
Desktop: refuse to republish an existing release version (#7941)
* Desktop: refuse to republish an existing release version * Desktop: harden immutable release publishing * Desktop: detect existing draft releases before publishing * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Desktop: verify reserved release tag * Desktop: update VirusTotal workflow assertions --------- Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
da72577146
|
Deduplicate the scan-packages baseline, and gate it with tests (#8110)
#8135 re-baselined the huggingface_hub 1.x backoff loop, so that half of this branch is done upstream. What is left is a defect it did not touch: the shipped baseline carries five openai entries twice. openai/_base_client.py C2 polling/beaconing loop openai/auth/_workload.py IMDS + network calls openai/resources/beta/responses/responses.py C2 polling/beaconing loop openai/resources/realtime/realtime.py C2 polling/beaconing loop openai/resources/responses/responses.py C2 polling/beaconing loop 223 entries, 218 distinct. A duplicated suppression is not harmless: whoever removes one copy to un-suppress a finding still gets no finding, because the second copy is still matching. The 111 tests here cover the baseline's shape and matching semantics, including the duplicate check that caught this. Every one passes against main once the five copies are gone. Co-authored-by: danielhanchen <unslothshared@gmail.com> |
||
|
|
302cf1b995
|
Desktop: prevent thin AppImage environment failures (#8055)
* Desktop: prevent thin AppImage environment failures * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Accept the unversioned AppIndicator names in the tray preflight * Desktop: harden AppIndicator preflight * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Stop the AppIndicator preflight at the first candidate the loader would use The loader commits to the first file it finds for a dlopen name, so a broken copy ahead of a working one fails the dlopen rather than falling through to a later directory. The search now walks one name at a time across LD_LIBRARY_PATH, the ldconfig cache and the default directories, and judges only the first readable match, so a host whose tray still crashes no longer passes the guard. * Tighten the AppIndicator preflight comments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> |
||
|
|
5087555049
|
release-desktop: fail closed when trusted-signing-cli is missing or broken (#8106)
* release-desktop: fail closed when trusted-signing-cli is missing or broken Both branches of the verification step ended in '|| Write-Output "..."', which exits 0. A missing or non-functional binary therefore reported success and the failure only surfaced later, inside the Tauri bundling step, as a signing error with no obvious cause. This matters more now that the binary is restored from a cache: a truncated or corrupt cache entry restores, skips the install step, and would otherwise pass this check unnoticed. The error messages name the recovery, since a bad cache entry survives until the version key changes. Reading $LASTEXITCODE is deliberate: $ErrorActionPreference does not trap a native binary exiting non-zero, so checking it is what makes the second branch meaningful. * Catch a binary that cannot be started, and keep the recovery on one line A truncated cache entry gives 'Exec format error', which is a terminating PowerShell error rather than a native exit code. Under $ErrorActionPreference = 'Stop' the step died on the invocation line, before reaching the branch that names the cache-bump recovery, so the corrupt-cache case this check exists for got no guidance. Catching it surfaced a second problem: the PowerShell error spans message, offending line and caret, and an annotation is truncated at the first newline, which dropped the recovery text. The message is now flattened, and the recovery comes ahead of the underlying detail so it survives clipping. |
||
|
|
ee64eec51a
|
release-desktop: add a VirusTotal pre-flight scan of the release bundles (#8089)
* release-desktop: add a VirusTotal pre-flight scan of the release bundles * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * virustotal_scan: register the signed upload URL with add-mask * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * virustotal_scan: check out the script, stop replaying single-use upload URLs, bound every request by the deadline * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * virustotal_scan: fail closed on malformed hash lookups and cap pacing by the deadline * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bound VirusTotal socket calls to the deadline and scan only validated releases - Pass a per-call socket timeout through the transport, clamped to the remaining scan deadline, so a request starting just before the deadline cannot consume the full 300s cushion ahead of the step timeout. - Retry a malformed upload acknowledgement instead of aborting, since the disclosure cost of the upload has already been paid at that point. - Move the scan after 'Create or validate versioned release' so a run that is rejected has not already uploaded all four bundles. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Defer non-draft release creation past the scan and cap retry backoff - Split 'Create or validate versioned release' into a validation step that runs before the scan and a creation step that runs after it. A dispatch with draft=false and a new tag previously published an empty release that stayed assetless for the length of the scan, and permanently so if the run was cancelled part way through. - Clamp the exponential retry backoff to the remaining deadline, so a 429 or 5xx arriving late cannot sleep past --timeout-seconds before the loop notices and writes its summary. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep release notes unconditional, fail closed on lookup errors, fix the permission test - Write desktop-release-notes.md in the validation step, which always runs. The updater metadata step reads it on every run, so leaving the write in the conditional create step broke reruns against an existing release. - Only treat a lookup as a missing release when gh reports 'release not found'. Any other failure now fails the step, rather than proceeding to disclose the bundles for a run that cannot publish. - Point test_release_desktop_permissions at the renamed validation step and assert the deferred create step and its gate. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Escape third-party text in the VirusTotal warning annotations Engine names, detection labels and API error strings are third-party data written straight into a workflow command. Actions truncates an annotation at the first newline and mis-parses a bare %, so a crafted or merely awkward detection string could drop the engine list exactly when the scan is trying to alert a maintainer. Mirrors _gha_escape in lockfile_supply_chain_audit.py, including the replace-% first ordering. * Never report an unanalysed bundle as clean, and escape the summary - A hash known to VirusTotal can have no completed analysis, in which case last_analysis_stats is absent and parse_stats yields all zeros. That row read as 'known to VirusTotal' with zero detections, which looks like 70 engines cleared a bundle that none of them scanned. Such a row now reports 'no completed analysis' with stats left unset, so it renders as dashes and cannot trip the threshold. The upload path polls until status is completed, so it only requires a stats object. - Escape third-party engine names, detection labels and error strings in the job summary. It is appended to GITHUB_STEP_SUMMARY and rendered as Markdown, so a newline ended the row and | opened a new cell. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Avoid a CodeQL clear-text-logging false positive on the skip message Interpolating API_KEY_ENV into the skip log trips CodeQL's py/clear-text-logging-sensitive-data rule at high severity, because the constant's name ends in _KEY. It only ever holds the env var name, never the value, but the repo uses CodeQL default setup so there is no config to filter the query on. Write the name out literally and pin it against the constant in test_missing_key_skips_without_failing so the two cannot drift. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
d24f0f175a
|
Security: pin the credential-send allowlist entry to the reviewed file (#8104)
* Pin the credential-send allowlist entry to the file it was reviewed against The evidence for "Harvests environment variables/secrets AND makes network calls" records the Request/urlopen calls but not the destination, so the unsloth_zoo/hf_xet_health.py entry added in the previous commit kept the same evidence hash after repointing _endpoint() at another host. Verified both ways: swapping the default endpoint and swapping the url assignment each leave the hash at 674a558c, so the entry would have gone on suppressing a credential send to an attacker-controlled host. Widening the network evidence to capture destinations would rehash and reopen around 78 unrelated entries across every network-involving check, so bind the one entry instead. Finding now carries file_sha256, and a baseline entry may pin it. Absent means unpinned and the key alone suppresses, exactly as before, so the other 216 keys are untouched. Present means the entry covers only those file bytes, so any other edit reopens the CRITICAL and forces a fresh review. --write-baseline carries a pin over from the baseline in effect, not from its own output path, which would drop pins whenever the output goes somewhere new. Only hf_xet_health.py is pinned, being the one entry here that transmits a credential. Any future change to that file re-reds the gate until someone re-reviews it, which is the right trade for a file that sends HF_TOKEN. The digest is taken from the published wheel through the scanner's own archive reader, not from a source checkout: the two differ, and a checkout digest never matches what CI scans. Tests: a pinned entry reopens on changed file bytes, an unpinned entry stays content-agnostic, --write-baseline preserves a pin, and check_py_file stamps the digest. Two existing tests move to the dict return type. 103 pass, and all three scan shards exit 0. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
2b18aced20
|
Scope the security suite's offline guard to the tests that want it (#8054)
* Scope the security suite's offline guard to the tests that want it
`tests/security/conftest.py` replaces `socket.socket` so a scanner reaching the
internet fails loudly. The fixture was session-scoped: a directory conftest
limits which tests a fixture APPLIES to, but a session-scoped one is still torn
down when the session ends, so the patch stayed installed for every test that
ran afterwards.
CI does not hit this. `studio-backend-ci.yml` runs `tests/` with
`tests/vllm_compat` and `tests/version_compat` ignored, and that selection is
green either way (4335 passed on the unfixed tree, no blocked sockets). The
pinned-symbol suites that fetch upstream sources run in their own workflow.
A plain `pytest tests/` does hit it, which is what a developer runs locally.
`security` sorts before `version_compat` and `vllm_compat`, so about 1300 of
their checks died on a socket this file had replaced:
RuntimeError: network access blocked by tests/security/conftest.py
Each passed alone and failed in the suite, in that order only, which reads as
upstream drift rather than as a fixture. Per-test scope keeps the guard exactly
where it was meant to be; the security suite is unchanged at 201 passing.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Observe the teardown, not just the installed guard
Every assertion in the regression test ran while the autouse fixture was still
active, so all six proved the blocker was installed and none could see what the
finalizer hands back. Emptying that `finally` left them green while the
cross-suite leak returned, which is the failure mode the file exists to stop.
Two additions. One drives the fixture's own generator and checks the restored
class, standing the outer guard down first so it is not nesting a second
install and restoring the blocker to itself. The other runs a nested pytest
over a miniature of the original layout -- the security suite, then an ordinary
test after it -- and asserts the second one gets a working socket, which is the
same ordering that cost about 1300 tests.
Confirmed both fail with the `finally` body emptied and pass with it intact.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Trim the comments in the leak tests
---------
Co-authored-by: danielhanchen <unslothshared@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
||
|
|
f1649ec9ef
|
Desktop: fix Linux AppImage compatibility on newer distributions (#7953)
* Desktop: build AppImages from the deb payload * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Desktop: keep AppImage release guidance concise * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Desktop: resolve AppImage output before verification * Desktop: preserve AppImage host runtime defaults * Desktop: fix thin AppImage updater artifacts --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> |
||
|
|
08cee0026e
|
Desktop: publish drafts without rebuilding (#7907)
* Desktop: publish drafts without rebuild * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Harden desktop updater promotion * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
d8a30a98ff
|
Retry macOS disk image stapling (#7787) | ||
|
|
0924253e5f
|
Desktop: notarize the final macOS disk image on release (#7615)
* Notarize the final macOS disk image in the desktop release * Require an accepted DMG notarization verdict * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
1dd2fc4583
|
tests: read checked-in files as UTF-8 instead of the platform default (#7438)
* tests: read checked-in files as UTF-8 instead of the platform default Path.read_text() with no encoding uses locale.getpreferredencoding(), which is UTF-8 on the Linux runners and cp1252 on a stock Windows install. Nine module-level reads of checked-in source files were relying on that default. studio/backend/routes/inference.py carries the DeepSeek tool-call token regexes, so it holds U+FF5C and U+2581. Under cp1252 that read raised UnicodeDecodeError on byte 0x81 at position 97806, and because the reads run at import time it took test_cancel_atomicity.py and test_cancel_id_wiring.py out at collection, not as failures. Green on CI, permanently broken for a Windows contributor running the suite locally. Adds a guard: at module scope there is no tmp_path fixture, so a bare read_text()/write_text()/open() there is always touching a checked-in file. That makes the rule mechanical enough to enforce with no allowlist, while staying quiet about temp-dir I/O inside test bodies where the platform default is harmless. The repo already spells this correctly in 464 other places; this only stops the stragglers coming back. * tests: cover import-time helper reads and keep the guard py3.9-safe Follows up on the Codex review: - add `from __future__ import annotations`, since `str | None` in `_offender` is evaluated at import on Python 3.9 and pyproject declares requires-python ">=3.9,<3.15". - widen the guard from module scope to import time. Class bodies and the bodies of module-level helpers called from an executing statement run during collection too, so `CODE = _extract_mixed_precision_code()` was the same hazard as an inline read. `if __name__ == "__main__":` blocks are skipped: pytest never executes them. - scan studio/backend/tests/ as well as tests/. Both trees are collected on Windows by separate CI jobs, and the offender that started this, test_tool_xml_strip.py reading routes/inference.py, lives there. Widening it surfaced seven more import-time reads of checked-in sources; all now name utf-8. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Harden the import-time encoding guard for PR #7438 Close the detector gaps raised in review, all of which I reproduced against the actual AST before changing anything. False negatives (the guard let a real hazard through): - _is_main_guard ignored the comparison operator, so if __name__ != "__main__" counted as script-only even though its body runs at import. - The else arm of a main guard was discarded with the rest of the If node. - Decorators and argument defaults on a module-level def were skipped with the body, though both are evaluated when the def executes. - Path.open() in text mode was invisible; only builtin open() was matched. - encoding = None and encoding = "locale" both re-select the platform default, but the keyword merely being present counted as pinned. False positives (the guard would have blocked a compliant contributor): - A non-literal mode fell through to the "r" default, so open(p, mode) was flagged even when mode is "rb", where adding encoding= is a ValueError and there is no edit that satisfies the rule. - Same for open(*args) and a **kwargs splat, which hide the mode and can hide an encoding. - Lambda bodies and comprehension elements were walked even though neither runs at definition. Verified: still reports the same 22 offenders on unpatched main, green on this branch and on the tree merged with latest main (557 files), and an adversarial corpus of 33 cases now scores zero false positives and zero false negatives. Also corrected two docstring claims: neither collecting job runs on Windows, and the read is governed by locale.getencoding(). * Walk eager comprehensions and treat io.open as the builtin Two regressions from the previous commit, both reproduced against the AST before changing anything. Lumping list, set and dict comprehensions in with generator expressions was wrong. Only a genexp is lazy; the other three run their element expression, their filters and their nested iterators immediately, so CONTENTS = [p.read_text() for p in PATHS] at module scope is an import-time read the guard was silently missing. Comprehensions are now walked in full and only the genexp keeps the outermost-iterable-only treatment. io was also in the not-a-path-opener list, but io.open is the builtin, with the same mode position and the same platform default. io.open(CHECKED_IN_FILE) is exactly the hazard this guard exists for, so it is matched now, with binary modes and a pinned encoding still exempt. tarfile.open and fitz.open stay exempt since neither has an encoding to name. Verified: 13 targeted cases covering all five eager comprehension forms and io.open in text, binary and pinned shapes all classify correctly; still 22 offenders on unpatched main; green on this branch and on the tree merged with latest main. * Close three more walker gaps in the import-time guard All three reproduced against the AST first. A generator expression handed straight to a call is consumed there, so DATA = "".join(p.read_text() for p in paths) runs its element at import. Only an unconsumed genexp bound to a name stays lazy, so the walker now follows the consumed ones in full and keeps the outermost-iterable-only treatment for the rest. if "__main__" == __name__ is an equivalent and accepted spelling of the main guard, but requiring __name__ on the left meant its body was treated as import-time code. That is a false positive on a block pytest never runs, so both operand orders are recognised now. The helper table was built from module-level defs only, so a def in a class body invoked while the class is constructed was never followed, contradicting the walker's stated coverage of class bodies. Helpers are now collected from the module body and from class bodies at any nesting. Verified: 15 targeted cases including all three fixes and the earlier ones still classify correctly; still 22 offenders on unpatched main; green on this branch and on the tree merged with latest main. * Handle positional read_text encodings, lazy generators and nested helpers * Guard reads reached from test bodies, unbound Path calls and __file__ paths * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Follow derived paths, skip lazy generator helpers, cover compressed openers * Guard the CLI tests, helper parameters and unbound Path arguments * Discover test roots and follow literal, in-place and tuple-derived paths * Identify module openers by import, unwrap starred paths, pin subprocess snippets * Resolve import origins, seed helper locals, follow named generators and parametrize * Scope imports lexically, list tracked test files, bind unpacked names * Resolve aliased openers, keyword-only params, destructured targets, next() * Pin the encoding on subprocess snippets, workflow lint and CLI output for PR #7438 * Harden the CLI encoding guard against detached streams for PR #7438 * Tighten the encoding guard's path and scope analysis for PR #7438 * Resolve path provenance more precisely and keep POSIX stream encodings for PR #7438 * Resolve qualified path classes and scope conditional imports for PR #7438 * Scope CLI stream setup to the entry point and align two encoding pairs for PR #7438 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> |
||
|
|
26faceecf7
|
Harden desktop release token permissions (#7172)
* Harden desktop release token permissions * Specify UTF-8 for workflow permission tests |
||
|
|
73d9653d5b
|
scan_packages: key baseline on matched-code hash so payloads in baselined files are not auto-suppressed (#6552)
* scan_packages: key baseline on matched-code hash The baseline matched on (package, package-relative file, check), which excluded the matched code, so a future finding of the same check in the same file was suppressed regardless of what the code did. A malicious future version of an already-baselined package could place a payload in the same file under the same check and pass the enforcing gate. Key the baseline on a hash of the matched code too. The hash is over the deduped, sorted set of matched spans with L<NN>: line markers stripped, so version bumps, line shifts and match reordering stay stable while new or changed flagged code reopens the finding. Version is left out of the key so routine dependency bumps do not reopen every entry. The hash is capped and recomputable from the stored evidence. Regenerate scan_packages_baseline.json against the current dependency set; the hf-stack, studio and extras scan shards pass enforcing (no active CRITICAL or HIGH). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan_packages: refresh baseline for newer unsloth-zoo release A newer unsloth-zoo published after the first regenerate added tests/test_mlx_save_export_regressions.py, a benign test fixture (temporary_location="/tmp/ignored") that trips the /tmp dropper check. Regenerate the hf-stack shard against the current set so the entry is allowlisted; studio and extras are unchanged. * scan_packages: harden baseline loading against malformed JSON Guard against a non-dict top-level baseline and non-dict entries so a corrupt or hand-edited allowlist warns and fails closed instead of crashing with AttributeError, and treat an explicit evidence: null as empty. * scan_packages: hash the full match set, keep indentation, strip only the marker Address the evidence-hash review feedback: - Capture every matching line, not the first three, so a payload appended after existing matches in a baselined file and check reopens the finding instead of riding the sample. - Preserve leading indentation so a flagged line moved out of a guarded block reads as changed. - Strip only each span's prefix up to the first L<NN>: marker, so an L<NN>: inside the matched code is kept and a change to it reopens the finding. Evidence and its hash are stored in full and stay recomputable from the stored field. Regenerate the baseline; hf-stack, studio and extras pass enforcing with no active CRITICAL or HIGH. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan_packages: bind baseline evidence to full matched code Address review feedback on the evidence-hash baseline key: - Split evidence only on real span delimiters (" | " before an L<NN>: marker, or a newline), so a bitwise-or or union type in matched code is no longer split apart into separate spans. - Record matched lines in full (drop the 160-char per-line cap) and record every distinct multiline match, so code appended past the cap or a second cross-line match reopens the finding instead of riding the first one. - Give the large-JS-bundle and .pth base64-blob findings a content digest instead of empty or prefix-only evidence, and record all .pth import lines, so a changed bundle, blob or import no longer inherits a baselined empty or truncated key. - Warn when a loaded baseline has entries without evidence_hash so a legacy baseline is regenerated rather than silently degraded. Regenerate scripts/scan_packages_baseline.json against the current dep set and add regression tests for each case. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan_packages: harden multiline and duplicate evidence handling Follow-up hardening so the evidence hash tracks the full matched code: - For DOTALL patterns that match across lines, record every line the match spans (not just the start line), so a change on a continuation line (the URL inside a baselined C2 loop, a swapped credential path) reopens the finding. A pathological greedy span is bounded to its head line plus a digest of the rest. - Keep duplicate spans in the canonical evidence so a second identical matched line in a new code path changes the key instead of deduping away. - Anchor the evidence prefix to strip only a genuine leading label or line-number marker, leaving a marker-like "L<NN>:" inside raw .pth code intact. - Make the legacy-baseline warning explicit that entries without an evidence_hash reopen rather than suppress under a coarse key. Regenerate scripts/scan_packages_baseline.json (same finding set; entries for same-file repeated checks are now tracked separately) and add tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan_packages: bind every combo and large finding to its full content Close the remaining asymmetric-evidence gaps so a changed payload cannot ride a reviewed baseline entry: - Digest a capped multiline span from the code without line markers, so a pure line shift stays stable while a continuation-line change reopens. - Give the "Unusually large executable .pth" finding a content digest instead of keying on byte size and import-line count alone. - Record both contributing signals for the JS credential+network stealer, the shell credential+network and persistence-hook combos, and the hidden network+exec docstring payload, so changing the network/exec side reopens. - Allow punctuation in an evidence label prefix so a "network+exec:" label is stripped and line shifts do not change the key. Regenerate scripts/scan_packages_baseline.json and add tests for each case. * scan_packages: bind remaining Python combos; key npm baseline on evidence Python scanner: the openssl+key, anti-analysis, DNS-exfil and base64+exec+blob combos recorded only one contributing signal, so a changed payload on the other side could ride a reviewed baseline entry. Each now binds every co-occurring signal (and the blob is digested, since it can sit on a separate line from the decode call). npm scanner: scan_npm_packages.py keyed its allowlist on (package, path, pattern) only, the same coarse-key bypass the Python scanner just closed. Add an evidence hash to the key (schema v3, fail-closed on older baselines) and store full evidence. The committed baseline stays empty by design. Regenerate scripts/scan_packages_baseline.json and add tests for each case. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan_npm_packages: bind full blob evidence and harden baseline loader Follow-up on the npm evidence-hash key: - _evidence now records every match and, when a snippet is truncated for display, appends a digest of the full match. The obfuscated-blob key was hashing only the truncated first-match snippet, so a changed payload tail or an appended blob in the same package/file/pattern could ride a reviewed entry. - _load_baseline guards that the root is an object, entries is a list, and each entry is a dict before reading it, so a malformed baseline warns and fails closed instead of raising AttributeError. Add tests for a changed blob tail reopening the key and for malformed entries. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan_packages: symmetric baseline-loader guards; bind npm outbound host context - Python _load_baseline now rejects a non-list "entries" with a warning instead of raising TypeError, matching the npm loader. - npm cred-surface-host (outbound) records the host with its URL path / fetch call / host config, so a changed outbound path, headers or body reopens the key rather than riding the bare host literal. Add tests for both. * scan_npm_packages: migrate v2 baselines and bind host-config outbound context - _load_baseline now migrates schema v2 entries by recomputing the evidence hash from stored evidence (with a legacy warning), matching the Python loader, instead of discarding them; only pre-v2 basename schemas are rejected. - The cred-surface-host (outbound) host-config branch now captures the whole line (path, headers, body), so a changed outbound payload on the same hostname line reopens the key instead of riding the bare host snippet. Add tests for v2 migration and the host-config context binding. * scan packages: bind PEM key bodies and npm windowed evidence to baseline keys scan_packages: embedded-key findings now pin the full PEM block (BEGIN..END) via a content digest, so a key body swapped under the same marker reopens the finding instead of riding the unchanged BEGIN line. Single-line and DER keys were already bound by their full matched line; marker-only references with no END block (validation header lists) are unaffected, so the committed baseline is unchanged. scan_npm_packages: _evidence now digests the full containing line whenever the shown snippet is only a window into it (short match on a long line, or a truncated payload), so a changed payload tail outside the display window reopens the key. The npm baseline is empty, so this changes no suppressions. Adds regression tests for both cases. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan packages: bind multi-line evidence and every blob to baseline keys _extract_evidence now extends each single-line match over its bracket continuations, so a multi-line call binds its argument lines and a changed URL or body on a continuation line reopens the key. After the per-line pass it also records cross-line matches the scan cannot otherwise see (a DOTALL regex, or a multi-line construct appended under a check that already had a one-line match), so an appended multiline payload reopens instead of riding the key. _blob_digest hashes every large base64 blob (not just the first) for the base64+exec finding and the .pth large-blob finding, so an appended or swapped second encoded payload reopens; single-blob files keep the same digest. scan_npm_packages _evidence digests the full logical line (the matched line plus its bracket-continuation lines), so a multi-line fetch's option and header lines bind and a changed payload on a following line reopens the outbound key. Regenerated the Python baseline: same package/file/check set, 24 entries pick up the wider multi-line evidence. Adds regression tests for each case. * scan packages: stop giant greedy spans from binding a whole-file digest When a greedy DOTALL pattern (reverse shell socket...subprocess, C2 loop) has its anchor tokens far apart, the match span covers the whole file. Digesting that span bound thousands of unrelated lines, so the evidence hash drifted on any edit between the anchors (a dependency bump reshuffling the file), which made a baselined finding reopen on an upstream release. The multiline pass now skips an oversized span when the per-line pass already bound the signal lines, so the evidence is the stable matched lines; a genuinely appended multi-line construct stays under the cap and is still recorded. Regenerated the Python baseline against Python 3.12 (the version the scan CI shards run) so the resolved dependency set matches CI. Same package/file/check set. Adds a regression test. * scan packages: tighten evidence binding (order, string brackets, span size) Address review follow-ups on the evidence extraction: - _canon_evidence keeps discovery (line) order instead of sorting. Line-shift stability already comes from stripping the L<NN>: markers, so order stays significant and reordering matched lines (a multi-line call's arguments) reopens the finding. - _logical_line_end (Python) and _logical_line_text (npm) blank string literals before counting brackets, so a ) inside a string argument does not close the logical line early and drop later argument lines. - The oversized-span skip now only drops a giant whole-file bridge (over 60 lines); a genuinely appended multi-line construct is recorded so its payload reopens, rather than riding an existing one-line match. - npm _logical_line_text binds the enclosing bracket group, so a host-config object whose { is on a prior line binds its path/headers/body lines. Regenerated the Python baseline (Python 3.12, matching the scan CI shards): same package/file/check set. Adds regression tests for each. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan npm packages: normalize and bound the logical-line digest - _evidence whitespace-normalizes the logical line before digesting (matching _evidence_hash), so a formatter-only reindent of the bound continuation lines does not change the sha256 suffix and reopen an unchanged finding. - _logical_line_text follows a bracket group to its close up to a hard 200-line cap (digest input only), so a config object longer than the backward window still binds its whole tail instead of silently truncating. Adds regression tests. npm baseline is empty, so no regeneration is needed. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan: cap single-line evidence and widen npm opener window Cap each rendered evidence line at 200 chars in scan_packages.py: a long or minified one-line file is shown as a bounded prefix plus a sha256 of the full line, so a packed payload cannot dump unbounded content into the CI logs or baseline while a change past the cutoff still changes the digest and reopens the finding. Mirrors how the npm scanner bounds its snippets. Widen the npm backward opener window (_MAX_CONT_LINES 12 to 200, symmetric with the forward cap) so a host deep inside a large options object binds the whole object, not just its own line; a changed path, header, or body on any property reopens. Regenerate the Python baseline with Python 3.12: only the protobuf nspkg.pth and unsloth-zoo compiler.py evidence change, both from the new line cap; the package/file/check key set is unchanged. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scan: bind all host contexts, deep call continuations, far-back npm openers Three fail-closed evidence gaps surfaced by review of the previous round. scan_npm_packages.py: measure the forward bracket-group cap from the matched line (idx + _MAX_GROUP_LINES) instead of the opener, so an opener found near the widened backward limit no longer consumes the forward budget and drops the path, headers, or body that follow the host. scan_npm_packages.py: _outbound_host_evidence now records every outbound context form for a host (URL, fetch-context, host-config), claiming each non-overlapping match in form order, so a separate host-config request added beside an already-baselined URL changes the evidence and reopens the key. The common single-context case keeps its existing snippet. scan_packages.py: follow a matched Python call over its continuations up to a separate _MAX_CALL_LINES (40), decoupled from the 12-line display threshold, so a multi-line requests.post( binds its whole argument list in the digest and a changed body deep in the call reopens; bounded so a miscounted bracket cannot swallow unrelated code. No baseline change: the current dependency set has no matched call that closes between 13 and 40 lines, confirmed by a Python 3.12 regenerate that produced a byte-identical baseline. * scan: clamp npm depth, pin large bundles, follow backslash and bound .pth dump Four fail-closed evidence gaps surfaced by review of the previous round. scan_npm_packages.py: clamp the backward opener scan at depth 0 so a leading unmatched closer (a preceding block whose opener is outside the backward window) no longer drives depth negative and masks the real enclosing opener that follows; a host-config object after such a block now binds and a changed path reopens. scan_packages.py: a large JS bundle now pins its whole content even when another JS heuristic already fired. The bundle digest was only added when no other finding existed; it is now appended to every finding's evidence on a large bundle, so an unchanged obfuscation signature no longer lets changed payload elsewhere ride the matched-line key. scan_packages.py: _logical_line_end follows explicit backslash line continuations, so a call split with a backslash before its parenthesis binds the continuation line (URL/body) instead of returning at the zero-depth API line. scan_packages.py: the catch-all .pth import evidence is bounded through _cap_line (prefix plus a digest of every line) so a large .pth of benign imports cannot dump the whole member into the logs or baseline while an appended or swapped import still reopens. Baseline regenerated with Python 3.12: key set unchanged; one entry (unsloth-zoo compiler.py) gains the backslash-continued banner lines now bound by the continuation fix. * scan: handle multi-line strings, lifecycle bodies, and de-quadratic evidence Addresses a review round plus a performance audit of the evidence extractor. Correctness (fail-closed): - Bind the UNION of the single-line-blanked and multi-line-blanked bracket spans in both scanners. The multi-line view blanks a triple-quoted Python string or a backtick template literal that spans lines, so a `)` inside such a string no longer closes the enclosing call early and drop later arguments. The single-line view still counts a payload embedded INSIDE a string, so a dropper that hides a call in a string keeps its argument lines bound. Taking the larger span never shrinks the binding below either view, avoiding a fail-open regression. - cred-env-in-lifecycle now pins the whole lifecycle script body via a digest, so a changed non-token line (e.g. adding a curl exfil beside the token reference) reopens, not just a change on the token line. Performance / DoS (the scanner runs on attacker-controlled package files up to the 64 MiB / 16 MiB member caps, with no per-file time budget): - _extract_evidence precomputes newline offsets once and maps match offsets with bisect, removing the O(matches) whole-file content.count per match that made the finditer fallback quadratic (a crafted minified file went from ~13 s/MiB and hours at the cap to linear). - npm _index_text splits and string-blanks the file once per evidence call instead of per match (was O(matches x file) time and allocation). - Bound evidence output: _MAX_EVIDENCE_SPANS (Python) and _MAX_EVIDENCE_MATCHES (npm) fold the remainder into a digest so a file with thousands of matches cannot build a multi-megabyte evidence/baseline blob while an added/removed match past the cap still changes the key. - _outbound_host_evidence caps matches per form and bounds the overlap claim so a host repeated many times cannot make it quadratic. No baseline change: a Python 3.12 regenerate is byte-identical (the union equals the legacy single-line span for every current dependency file; the cap thresholds sit above the largest real entry), so these are forward-looking hardening with no drift. * scan: count all overflow matches, bind their context, blank JS regex literals Follow-ups on the evidence output caps from the previous commit. - _outbound_host_evidence no longer truncates each pattern's match iterator with islice; it iterates every match and runs the overlap dedup only while the display list is below the cap (so claimed stays bounded and the check is O(cap) per match, not quadratic), folding every match past the cap into the overflow digest. A host context beyond the 64th is counted again, so it reopens. - The overflow digest (both scanners, via a shared _overflow_digest) binds each overflow match's logical-line context, not just the regex match text, so a changed payload on an over-cap line reopens even with the matched token unchanged. - The multi-line JS blanked view now blanks regex-literal bodies (tracking the previous significant char for regex-vs-division and char classes for a literal `/` inside `[...]`), so a `)` inside `/)/` no longer closes an outbound call early. The bound span is the union of the single-line and multi-line views, so an imperfect regex decision only ever grows the span, never shrinks it. - The Python overflow digest canonicalizes spans (strips L<NN>: markers via _canon_evidence) before hashing, restoring line-shift stability for the over-cap region. No baseline change: the overflow branches only trigger above the per-finding caps (above the largest real entry), and the npm baseline is empty, so a Python 3.12 regenerate is byte-identical. * scan: refresh baseline for ipython interactiveshell.py span drift A newer ipython release changed the filesystem-enumeration span in IPython/core/interactiveshell.py, so its content digest no longer matched the baselined evidence and the studio scan shard flagged it as a non-baselined CRITICAL. Regenerated with Python 3.12: only the ipython entry's evidence_hash changes; the package/file/check key set is unchanged, and a studio enforcing spot-check exits 0. * Bound scanner evidence memory: stream overflow spans and cap lifecycle baseline size scan_packages.py: _extract_evidence no longer materializes a rendered span per match before slicing at the display cap. Once out holds _MAX_EVIDENCE_SPANS spans, further spans fold straight into a running digest, so a minified or padded file with hundreds of thousands of matching lines keeps memory bounded to the display cap instead of the match count. The fold reproduces _canon_evidence(" | ".join(overflow)) byte for byte, so the overflow digest and every baseline key are unchanged. scan_npm_packages.py: lifecycle-fetch-exec and cred-path-in-lifecycle stored the entire install script body as evidence, so --write-baseline on a package with a multi-MiB lifecycle script bloated the baseline JSON. Both now store a bounded matched snippet plus a body-sha256 digest, matching cred-env-in-lifecycle. The digest still binds the whole body, so a change to any line reopens the finding. Adds tests for the streamed overflow bound and the bounded-but-reopens lifecycle evidence. Baseline unchanged (byte-identical Python evidence; npm baseline empty). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Make npm bracket-group scan order-aware so a same-line close-then-open binds _scan_group counted brackets with a per-line net (opens minus closes), which collapses intra-line order: a line that closes a prior block and then opens the host-config object on the same line, e.g. `}); const opts = {`, nets to <= 0, so the trailing `{` was dropped and the group started at the hostname line. A changed path/headers on the following lines then hashed to the same evidence and could ride an existing baseline key. Replace the net count with an order-aware (L, R) reduction per line (L closers needing an opener to the left, R openers needing a closer to the right) and apply it in order in both the backward and forward scans, clamping stray closers at 0. The trailing opener now stays visible so the whole object binds and a changed payload reopens. Per-line cost is unchanged (one C-level bracket findall), so the existing outbound-host evidence is byte-identical on all prior shapes; only the previously-dropped same-line case changes. Adds a regression test for it. * Harden scanner evidence: bound memory and bind Python call tails fail-closed Five fixes across both scanners, none of which change the committed baseline (a full regen of all three pip shards produced a byte-identical 185-key set). scan_npm_packages.py: _evidence and _outbound_host_evidence collected every regex match into a list before applying the 64-match display cap, so a text file under the size cap that repeats a cheap signal (such as NPM_TOKEN) millions of times could allocate a huge list of re.Match objects and stall or OOM before the overflow digest ran. They now stream from finditer and fold overflow as matches arrive via a shared _fold_overflow_match helper, byte-identical to the prior digest. scan_packages.py: - _extract_evidence kept inserting every unique over-cap span into the seen set even after it stopped appending to the display list, so a generated file with millions of one-line matches still grew that set unbounded. It now tracks spans only while filling the display list (per-line spans are unique by line number, so dropping them past the cap cannot miss a dedup). - _scan_line_end counted brackets with a per-line net, so a continued statement that closes on the same line it opens a flagged call (a leading "]" before "requests.post(") had the call's open paren cancelled and bound only the opener line. It now applies brackets in order via _bracket_lr (leading closers clamp at 0), matching the npm bracket fix. - a single-quoted string continued by a trailing backslash was not tracked across lines, so a close paren inside the continued string on the next line closed the call early; _blank_code_strings now carries the continuation. - a call with more argument lines than the soft cap was hashed only through the cap, so a changed data=/headers tail past it stayed suppressed; a closing call is now followed to its real close under a 200-line hard limit (a never-closing opener still stops at the 40-line soft cap so it cannot swallow the file). Adds regression tests for each. npm baseline is empty; the Python baseline is unchanged (verified byte-identical by regenerating all three shards). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bind giant DOTALL span anchors and add context to constant IOC evidence Two fail-closed gaps where a changed payload could keep the same evidence hash and stay suppressed by the baseline. scan_packages.py: a giant greedy DOTALL span (a cross-line IOC match bridging more than 60 lines, e.g. RE_TEMP_EXEC matching a /tmp line and a much-later subprocess line) was dropped entirely once the per-line pass had any match, so an appended cross-line payload -- a new /tmp line plus a later subprocess line that share no single line, so the per-line pass never binds them -- produced the same evidence and rode the key. The span is no longer dropped: it is bound by its head and tail anchor lines plus a digest over just those (no line numbers, so a pure line shift is stable). An added or moved anchor reopens the finding, while churn in the bridged interior stays stable, so this does not reintroduce whole-file drift. Two baseline entries (multiprocess test, unsloth-zoo scanner file) carry such a span and are refreshed; a full three-shard regen confirmed only those two keys change. scan_npm_packages.py: known-ioc-string and cred-surface-host (always-bad) recorded only the bare needle/host as evidence, so a reviewed tarball that kept the IOC string while altering the adjacent fetch/exfil body produced an identical key. They now bind matched-line context: known-ioc-string via the matched line and its bracket-group continuation, cred-surface-host (always-bad) via the outbound call context (path/headers/body, falling back to the bare host when not in an outbound call). A changed payload on the same call now reopens. Adds regression tests for each. npm baseline is empty; the Python baseline updates only the two giant-span entries. * Hash giant-span interiors, bind exec/eval trigger, JS content, intra-literal whitespace Four fail-closed gaps where a changed payload could keep the same evidence hash. scan_packages.py: - A giant bridged DOTALL span was bound only by its head and tail anchors, so a cross-line payload inserted into the bridged interior between unchanged outer anchors kept the same key. The whole span content is now digested (via _render), so any interior change reopens; a pure line shift stays stable because the digest is over the markerless code. Two baseline entries (multiprocess test, unsloth-zoo scanner file) carry such a span; with full-interior binding, multiprocess resolved at two versions across shards now yields two distinct entries where the anchor digest had collapsed them into one. - The exec/eval-with-hidden-payload findings omitted the visible exec/eval line that makes the hidden string executable, so flipping a harmless eval("1+1") to exec(__doc__) kept the same key while arming the payload. The trigger line from the real-code view is now bound into the evidence. - check_js_file extracted evidence with the Python-string-aware extractor, which does not blank JS backtick template literals, so a template containing a close paren closed a call's bracket span early and omitted later option/body lines. The full file content digest is now pinned to every JS finding (not just large bundles), binding the whole call. scan_npm_packages.py: the evidence canon collapsed all whitespace via split(), erasing whitespace inside JS string literals along with harmless indentation, so a changed request body 'a b' -> 'a b' kept the same key. A new _canon_preserve_strings collapses whitespace only OUTSIDE string literals (reindent-stable) while preserving it INSIDE single/double/backtick literals (intra-payload edits reopen). Used for the evidence hash and the logical-line digests. Adds regression tests for each. npm baseline is empty; the Python baseline updates the two giant-span entries and adds the second multiprocess version's entry. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
07c7f9bfca
|
Package scanners: close fail-open gaps in the sdist fallback and hidden-payload paths (#6359)
* Package scanners: close fail-open gaps in the sdist fallback and hidden-payload paths Follow-up hardening on the now-blocking scanners so the enforcing gate cannot report clean while a malicious artifact goes unscanned. scan_packages.py - Hidden payload: also flag a network call AND an os/subprocess exec that live only in a blanked docstring/string of an exec/eval file (the fetch-then-run shape of an exec(__doc__) dropper). Either alone in real code was already covered; hidden together they are the payload. - Pinned releases fail closed: _release_files no longer falls back to the latest artifact when a pinned version is missing or empty, so a yanked/bad pin is an error instead of a different file being scanned in its place. - requires_dist is read from the pinned release's metadata, not the project-level (latest) document, so a sdist-only pin follows its own dependency tree. - Environment markers are evaluated (PEP 508) instead of dropping any marker that merely contains the word extra, so default-true markers like extra != 'dev' are kept; conservative fallback keeps a dep on any uncertainty. - Transitive recovery is a depth-bounded worklist: a wheel dependency whose own child is sdist-only is fetched (--no-deps) and scanned, then its children are recovered in turn, rather than being silently skipped. scan_npm_packages.py - Baseline keys use the package-relative path instead of the basename, so the same basename in a different directory is not over-suppressed. Tests cover each case; full scripts pass AST and ruff checks. * Address review: tighten marker scope, decoy-proof the dropper check, fail closed on missing pin metadata - Markers: keep any dep whose marker can hold on another install target (sys_platform == 'win32', python_version == '3.13'); only drop a marker that depends solely on extra and is false with no extra. A scanner runs on one target but must cover code installed on others. Pure-extra markers are evaluated against default_environment() with extra unset. - Hidden dropper: the network+exec docstring check now inspects the removed (blanked) span directly, so a benign visible network or subprocess call cannot mask a payload that still lives in a docstring. Carrier checks stay blanked-only (an in-code carrier is already caught by the normal check), so corpus findings are unchanged. - requires_dist: a pinned version whose own metadata cannot be fetched recovers nothing rather than substituting the latest release's dependency tree. - Transitive recovery: the last-ditch direct-sdist branch also chases the recovered package's declared deps, matching the other branches. - npm baseline: schema bumped to v2 (package-relative keys); a pre-v2 baseline with entries is ignored (fail closed) instead of mis-applying basename keys. Tests cover each case; scripts pass AST, ruff, and the import-hoist verifier. * Scanner: exclude comments from hidden-payload check, flag missing pin metadata as incomplete Hidden network+exec detection now inspects only docstring/string spans (what exec(__doc__)/exec(<str>) can actually run), so a real exec() beside comments that mention a network and a subprocess call no longer false-positives. Missing pinned-release metadata in transitive recovery records a download_error so the --with-deps path fails closed instead of treating it as no dependencies. Adds regression tests for both. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
892d2983b0
|
Fix: scan_packages.py --fix crash on download_packages() tuple return (#6413)
* Fix scan_packages.py --fix crash on download_packages() tuple return `download_packages()` returns `(results, download_errors)`, but the two `--fix`-path call sites still treated the return value as the bare results list. `find_safe_version` did `downloaded = download_packages(...)` followed by `if not downloaded:` (always false: a 2-tuple is truthy) and `for _, archive_path in downloaded:`, which unpacked the results list into two variables -> ValueError in the normal single-archive `--no-deps` case. `_run_fix` indexed `downloaded[0][1]`, i.e. the second archive of the results list instead of the first archive's path -> IndexError. So `--fix` crashed exactly when a CRITICAL finding needed remediation. The main scan path already unpacks the tuple; this aligns the two `--fix` sites with it. Adds CPU-only regression tests for both sites. Closes #6412 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update scripts/scan_packages.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update scripts/scan_packages.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> |
||
|
|
a6dc10dad2
|
Reduce and tighten comments and docstrings across the test suite (#6429)
Some checks are pending
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Studio load-orchestrator CI / test (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
* Reduce and tighten comments and docstrings in tests Shorten verbose comments and docstrings across the test suite without changing any test logic. Remove narration that restates the next line, collapse long module and test docstrings to a single line, and drop banner separators. Keep regression context (issue and PR references, run ids), skip reasons, mocking and timing rationale, license headers, lint and type directives, and commented-out code. Comments and docstrings only: an AST signature check confirms no code, assertions, or string literals changed, and the suite byte-compiles cleanly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
21612c2e32
|
Package scanners: cut false positives and make the CI gate blocking (#6355)
* Package scanners: cut false positives and make the CI gate blocking scan_packages.py and scan_npm_packages.py red-failed on legitimate library code, so the security-audit steps were left advisory. Reduce the false positives at the source and flip both gates to blocking. scan_packages.py: - Scan code only: blank comments and bare docstrings/doctests before matching (line numbers preserved), so prose and >>> examples cannot trip a finding. - Drop the platform.system() branch from the anti-analysis regex (under DOTALL it matched across the whole file, so every cross-platform library tripped it) and fix the dead /proc/self/status alternative. - Add a reviewed baseline allowlist (scan_packages_baseline.json) keyed on (package, basename, check): only non-baselined CRITICAL/HIGH exit 1, and a new kind of finding in a listed file still fails. - sdist fallback: when --with-deps cannot resolve a shard (a sdist-only package or a version conflict), drop to per-spec and fetch the raw sdist from the PyPI JSON API (no pip build, no setup.py), so every package is still scanned and no shard exits 2. scan_npm_packages.py: - Mirror the code-only JS/TS scanning (blank // and /* */ comments, string/template/regex aware) and the baseline allowlist. The npm corpus is clean today, so the baseline is empty. security-audit.yml: - Flip both scan steps to blocking (SCAN_ENFORCE=1), capturing the scanner exit via PIPESTATUS so tee does not mask it. tests/security: add coverage for the strip, baseline and sdist paths. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address review feedback on the package scanners - Do not blank f-strings during code-only scanning (they evaluate at import); and when a file uses exec/eval, rescan the original for payload carriers hidden in a docstring/string so exec(__doc__) style payloads stay visible. - sdist fallback: recover transitive deps with their version specifier (fetch the pinned version, not latest), and recover deps in the --no-deps branch too so a sdist-only transitive dependency is still scanned instead of silently skipped. - Baseline: key by package-relative path, not basename, so a future same-named file in another directory is not auto-suppressed. Regenerated the baseline accordingly. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
187144d4e7
|
Reduce and tighten code comments and docstrings repo-wide (#6095)
Trim and tighten code comments and docstrings across the repository. Comment-only: every changed file verified code-identical to main via AST/token comparison. |
||
|
|
3ce187da02
|
Formatting: ruff line-length 100, kwarg-spacing passes, drop blank after short local imports (#6079)
Raise ruff line-length to 100 and extend the local pre-commit format pipeline (def-signature magic-comma normalization, short multi-line assert collapse, kwarg '=' spacing, blank-line-after-short-import removal, adjacent string-literal / f-string+plain merge, redundant-pass pruning). Every transform re-checks the file AST and is dropped if it would differ; the whole-repo reformat is verified AST-identical per file and idempotent. |
||
|
|
dd0b557794
|
ci: advisory lockfile supply-chain audit (no install-script changes) (#5604)
* ci: add advisory lockfile supply-chain audit Adds a fast, focused workflow that scans every checked-in npm and cargo lockfile on PRs touching one. Default behaviour is advisory: only public indicator-of-compromise strings, versions on the public known-malicious list, and structurally broken lockfiles fail the build. Structural anomalies (missing integrity hashes, non-default registry, etc.) surface as :⚠️: annotations without gating merges, so reviewers see the audit result inline on every PR without changing the existing install behaviour. Also commits the two missing npm lockfiles the audit needs: studio/package-lock.json (Tauri CLI holder for desktop release) and studio/backend/core/data_recipe/oxc-validator/package-lock.json (oxc-parser runtime for the data-recipe validator). studio/setup.sh, studio/setup.ps1, build.sh, and pyproject.toml are intentionally left alone so the existing install path keeps working unchanged. Audit script behaviour: default mode -> exits 1 only on blocked-known-malicious, known-ioc-string, malformed-lockfile, missing-lockfile, unreadable-lockfile, or missing-toml-parser --strict -> promotes every finding to blocking (opt-in) Adds a try/except around lockfile reads so a permissions error prints a finding instead of crashing CI with a raw traceback. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * test(security): update cargo regression test for advisory mode `scripts/lockfile_supply_chain_audit.py` now classifies `non-registry-cargo-source` as an advisory finding by default (returns exit 0 with a `:⚠️:` annotation) rather than unconditionally blocking with exit 1. Update the existing `test_malicious_cargo_lockfile_refused` to pass --strict so it keeps verifying the "refuse to install" behavior it is named for, and add a second test that pins the default-mode behavior: advisory finding emitted, exit code 0. * audit: escape Finding for GH Actions annotations `:⚠️:` and `::error::` workflow commands truncate the annotation message at the first newline unless the message is %-encoded per the workflow-commands spec. Since `Finding.__str__` returns three lines (kind+path, package, detail), the package and detail fields were being dropped from the GitHub Actions UI. Add a `_gha_escape()` helper that applies the spec'd escapes (`%` -> `%25`, then `\r` -> `%0D`, then `\n` -> `%0A`; the `%` replacement must happen first so the subsequent escapes are not double-encoded), wrap every Finding rendered into a workflow command with it, and pin both the helper and the end-to-end single-line emission with two new regression tests. Caught by gemini-code-assist on PR #5604. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |
||
|
|
ef9f672fe8
|
security: NOT affected by Mini Shai-Hulud (May-12 wave) -- forward-looking hardening only (#5397)
* scripts/scan_*: add Mini Shai-Hulud May-12 IOC strings and pin-blocklists Append the May-12 2026 wave indicators (git-tanstack.com, transformers.pyz, /tmp/transformers.pyz, "With Love TeamPCP", "We've been online over 2 hours") to all three scanner IOC tables, add BLOCKED_NPM_VERSIONS (42 TanStack pkgs, 4 opensearch versions, 3 squawk pkgs) in scan_npm_packages.py and lockfile_supply_chain_audit.py (kept byte-identical), add BLOCKED_PYPI_VERSIONS (guardrails-ai 0.10.1, mistralai 2.4.6, lightning 2.6.2/2.6.3) plus RE_MAY12_IOC wiring across check_py_file/check_shell_file/check_workflow_file in scan_packages.py. The npm orchestrator and the lockfile auditor now short-circuit on a blocked entry before fetching the tarball, and the PyPI download pipeline drops blocked specs before pip download is invoked. * tests/security: regression suite for supply-chain scanners Adds offline fixture corpus and pytest coverage for scan_npm_packages, scan_packages, and lockfile_supply_chain_audit so future IOC-table drift surfaces at PR time. Pytest scope narrowed to tests/security so GPU smoke tests are not picked up by default. * ci(security-audit): drop continue-on-error on pip-scan and npm-scan jobs Promote three harden-runner blocks to egress-policy: block with per-job allowlists. Add tests-security job running pytest tests/security as a hard gate. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scripts: harden third-party downloads, pip resolver pins, atomic writes Pins uv installer and mlx_vlm qwen3_5 patches by commit SHA + SHA-256 checksum, scrubs PIP_* env vars and forces --index-url + --only-binary on pip download, applies tarbomb caps to scan_packages archive walks, and converts non-atomic config writes (kwargs spacer, studio stamper, notebook validator, scan_packages req-file fixer) to mkstemp+os.replace. Also adds host allowlist to notebook_to_python downloader, threads an --allow-shell flag through its shell=True emission with reviewer warning comments, locks both MLX installer scripts to set -euo pipefail, and extends CODEOWNERS so colab snapshot data files require notebook-owner review. * ci(workflows): harden release-desktop / smoke / notebooks workflows Pin dtolnay/rust-toolchain to a 40-char SHA, scope release-desktop permissions to read at workflow level with job-level write only on the build job, append --ignore-scripts to every npm ci / npm install in studio-frontend-ci / wheel-smoke / studio-tauri-smoke / release-desktop, validate client_payload.ref shape via an env-var-isolated regex on every notebooks-ci job, and add step-security/harden-runner in audit mode as the first step of release-desktop and mlx-ci. * scripts: promote silent scanner failures to non-zero exit codes scan_packages now returns 2 on pip-download failure and emits a CRITICAL archive_corrupted finding on truncated wheels/sdists. notebook_to_python exits 1 on per-notebook failures; notebook_validator wraps the stash/pop in try/finally; lockfile audit rejects bare UNSLOTH_LOCKFILE_AUDIT_SKIP=1 with a loud GitHub Actions warning. * Add npm cooldown + new-install-script gate + Dependabot cooldown Pins min-release-age=7 (npm 11.10+) in repo-root and studio/frontend .npmrc, adds scripts/check_new_install_scripts.py to fail PRs that add a postinstall dep, ships a new security-audit job for npm audit signatures plus the diff, and extends .github/dependabot.yml with cooldown stanzas. Pin @tanstack/react-router to 1.169.9 per GHSA- g7cv-rxg3-hmpx; lockfile regen deferred until that release lands on npm. tests/security gains 4 new tests; full suite 26/26 green. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ci(security): fix tanstack pin, exec bits, expand IOC tables to @uipath/@squawk full - Revert --ignore-scripts on Studio install workflows: vite build needs esbuild's native postinstall (per PR #5392 rationale). Keep --ignore-scripts on security-audit.yml's standalone npm audit job. - Pin @tanstack/react-router to the actual published 1.169.2 (was a forward-looking 1.169.9 that does not exist on npm; broke npm ci). - Drop redundant repo-root .npmrc; studio/frontend/.npmrc covers the only npm project today (root cooldown re-instate via dependabot.yml). - Restore exec bits on 7 files my filesystem stripped during cherry-pick. - Expand BLOCKED_NPM_VERSIONS with full safedep.io + Aikido enumeration: 22 @squawk/* packages with 5 versions each (110 entries; previously 3 entries with 1 version each), and 66 @uipath/* packages (entirely missing before). Mirror in scripts/lockfile_supply_chain_audit.py. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * tests/security: suppress CodeQL py/incomplete-url-substring-sanitization The two flagged 'X' in Y assertions are NOT URL sanitization checks. They verify our scanner WROTE a known IOC literal into its stdout / Finding.evidence, which is the opposite of an attack surface -- matching the scanner's output is precisely what catches the worm. Inline lgtm[] suppression with a 4-line rationale comment above each. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scripts/scan_*: expand IOC tables with Aikido full 169-pkg enumeration Per Aikido 2026-05-12 disclosure (373 malicious package-version entries across 169 npm package names), add to BLOCKED_NPM_VERSIONS: - @mistralai/* npm scope (3 packages, 9 versions) -- separate from the PyPI mistralai package already in BLOCKED_PYPI_VERSIONS - @tallyui/* (10 packages, 30 entries) - @beproduct/nestjs-auth (18 versions 0.1.2..0.1.19) - @draftlab/* + @draftauth/* (5 packages) - @taskflow-corp/cli, @tolka/cli, @ml-toolkit-ts/*, @mesadev/*, @dirigible-ai/sdk, @supersurkhet/* - 10 unscoped packages (safe-action, ts-dna, cross-stitch, cmux-agent-mcp, agentwork-cli, git-branch-selector, wot-api, git-git-git, nextmove-mcp, ml-toolkit-ts) Also add to KNOWN_IOC_STRINGS / NPM_IOC_STRINGS: - router_init.js SHA-256 ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c - tanstack_runner.js SHA-256 2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96 - bun run tanstack_runner.js marker (the new Bun-prepare-script dropper invocation pattern unique to this wave) Total: 170 packages, 401 versions blocklisted. Studio lockfile still scans clean (0 findings, 0 hard errors). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scripts/scan_*: web-verification additions (@tanstack/setup, intercom-client) Two findings from cross-checking BLOCKED_NPM_VERSIONS / KNOWN_IOC_STRINGS against GHSA-g7cv-rxg3-hmpx + Aikido + safedep.io + Socket + Semgrep. - Fix asymmetry: @tanstack/setup IOC string was in lockfile_supply_chain_audit.py's NPM_IOC_STRINGS but missing from scan_npm_packages.py's KNOWN_IOC_STRINGS. The literal is the malicious optional-dependency name used by the May-12 TanStack wave; no legitimate npm package of this name exists. - Add intercom-client@7.0.4: the npm counterpart of the lightning 2.6.2/2.6.3 PyPI compromise (Apr-30 wave). Same threat actor (TeamPCP). Confirmed by Semgrep, Aikido, OX Security, Resecurity, Kodem. Safe version is 7.0.3 and earlier. Total BLOCKED_NPM_VERSIONS: 171 packages / 402 versions. Both files remain byte-identical. Studio lockfile still scans clean. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ci(security): add workflow-trigger lint refusing pull_request_target + cache-poisoning vectors The two patterns that together powered GHSA-g7cv-rxg3-hmpx (TanStack Mini Shai-Hulud) are now gated at PR time: 1. pull_request_target -- the worm chain started with a fork PR that ran in the base-repo context. Every workflow in this repo today uses 'pull_request' (safe); the lint refuses any new pull_request_target additions outright. workflow_run is restricted, allowed only with an explicit allow-comment. 2. Shared cache keys between PR-triggered workflows and the publish workflow (release-desktop.yml). The TanStack attack chain poisoned a shared Actions cache from a fork PR; the legitimate release workflow then restored the poisoned cache. The lint refuses any cache key that appears in both a PR-triggered workflow and a workflow_dispatch-only / publish workflow. Current tree is clean: 0 pull_request_target, 0 workflow_run, 0 PR-publish cache-key collisions across all 24 workflows. The lint locks that invariant in place. Files: + scripts/lint_workflow_triggers.py (~200 LOC, stdlib + PyYAML) + tests/security/test_lint_workflow_triggers.py (5 tests covering current-tree pass, pull_request_target reject, workflow_run restricted, justified workflow_run accept, cache-key collision reject) ~ .github/workflows/security-audit.yml: new workflow-trigger-lint job, no continue-on-error, harden-runner block-mode, PyYAML only runtime dep. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * security: fix tests-security CI job + CodeQL false-positives Two CI failures on the prior push: 1. pytest tests/security -- 5 lint regression tests failed because scripts/lint_workflow_triggers.py imports PyYAML which is not in the bare runner's Python env. Added pyyaml==6.0.2 to the pip install step alongside pytest. (29 scanner tests already passed.) 2. CodeQL py/incomplete-url-substring-sanitization fired on two test assertions that check the scanner WROTE the IOC literal to its own stdout/stderr. The rule pattern-matches on `"<host>" in <var>` and cannot distinguish a URL sanitizer from a regression-test evidence check. Previous `# lgtm[...]` inline suppressions were detached from the operator when pre-commit reformatted the assert across multiple lines. Rebuilt the IOC literals at runtime (`"git-tanstack." + "com"`) so no URL-shaped source literal appears on the `in` operator line; rule cannot trigger. Verified locally: `pytest tests/security -v` -> 34 passed in 2.70s. * security(studio): defensive .npmrc cooldown aliases + save-exact Two additions to studio/frontend/.npmrc to harden the existing `min-release-age=7` (Mini Shai-Hulud defence): 1. `minimum-release-age=10080` (minutes) -- defensive alias for the same 7-day floor. Some npm versions / wrappers consult one key but not the other; setting both prevents a single upstream setting-name parse change from silently disabling the cooldown. The two keys MUST agree (do not let them drift). 2. `save-exact=true` -- refuses to write back `^x.y.z` ranges into package.json when a maintainer runs `npm install <pkg>` locally. Does NOT rewrite already-present ranges; stops NEW carets from creeping into the manifest as patch-version footguns. Verified: pytest tests/security -> 34 passed in 2.63s. * chore(dependabot): remove dead bun entry for /studio/frontend `package-ecosystem: "bun"` at /studio/frontend was a no-op: that path commits package-lock.json, not bun.lock / bun.lockb, so Dependabot's bun ecosystem silently skipped it. The actual behaviour is unchanged -- the npm entry below the cargo block already owns npm_and_yarn security advisories for /studio/frontend with `open-pull-requests-limit: 0` (version-update PRs suppressed, security PRs flow through). This commit: - Deletes the bun entry (kept a placeholder comment so a future bun migration knows where to slot it back in). - Rewrites the npm /studio/frontend entry comment to explain the real intent: lockfile is the authoritative pin, .npmrc `min-release-age=7` already blocks fresh tarballs at install time, dependabot only needs to surface security advisories. No functional change: same set of dependabot PRs as before (zero version updates, security advisories grouped weekly with cooldown). Verified: pytest tests/security -> 34 passed in 2.67s; YAML parses cleanly via PyYAML. * fix(dependabot): drop unsupported semver-* cooldown keys on github-actions Dependabot's validator rejected the config with: The property '#/updates/0/cooldown/semver-minor-days' is not supported for the package ecosystem 'github-actions'. The property '#/updates/0/cooldown/semver-patch-days' is not supported for the package ecosystem 'github-actions'. The `semver-minor-days` / `semver-patch-days` cooldown knobs are only valid for semver-aware ecosystems (npm, cargo, etc.). The github-actions ecosystem pins via git tags / SHAs, not semver, so only `default-days` is honored. Pre-existing bug on main; surfaced on this PR because the prior commit re-validated the file. Behaviour: github-actions PRs now respect the 7-day cooldown floor (was already the intent), without the no-op semver bands. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> |