Commit graph

42 commits

Author SHA1 Message Date
Daniel Han
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.
2026-08-20 00:05:28 -07:00
oobabooga
de102032ee
Desktop: ship a complete Linux AppImage (#9113)
---------

Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-08-19 10:09:29 -03:00
Daniel Han
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 fc325f431, on all three
pip scan-packages legs, with a single un-baselined CRITICAL:

  C2 polling/beaconing loop detected
  huggingface-hub  huggingface_hub/utils/_http.py
  L461:     while True: sha256:b087631...

No repo commit caused it. fc325f431 changed workflows and one test file, nothing
that resolves a dependency. What moved is upstream: the resolved huggingface-hub
came off the 0.x line, and 1.26.1, 1.27.0 and 1.28.0 all carry this loop while
0.36.2 does not. Reproduced locally against each of those versions, and the sha256
matches CI's byte for byte.

The code is `http_backoff`: it counts nb_tries against max_retries, sleeps with
exponential backoff between attempts, and raises once the budget is spent. A
bounded retry, not a beacon. RE_C2_POLLING is `while True .* sleep .* requests\.`
under re.DOTALL, which cannot tell those apart, so the file is allowlisted rather
than the check weakened -- consistent with the 51 CRITICALs already reviewed this
way, four of them this same check in this same package.

Added with the scanner's own --write-baseline so the evidence_hash is computed by
the code that will match it, then merged as a single entry rather than by
regenerating: a full rewrite re-sorts the file and turns a one-entry review into a
731-line diff. Nothing else added or removed, asserted on both directions of the
key set.

Worth recording: this file now holds FOUR entries for this check, at L298, L461,
L462 and L461 again, one per revision of the loop huggingface-hub has shipped.
That is the mechanism working -- the key is digest-pinned, so every edit reopens
the finding for review -- but it means a hub release touching those thirty lines
turns Security audit red again. The new test says so, so the next one is read as
upstream drift rather than a break.

test_the_hf_backoff_suppression_is_narrow guards the part that actually matters
about silencing a CRITICAL in a file that already speaks HTTP: every entry stays
pinned to reviewed code, none are duplicated, and a beaconing loop appended to the
same file under the same check produces a different key rather than inheriting the
suppression. Three mutations checked red (drop the entries, strip the sha256 pin,
duplicate an entry).

Verified: scan of huggingface-hub 1.28.0 goes 1 CRITICAL to 0, exit 1 to 0.
tests/security/test_scan_packages.py 123 passed.

* [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>
2026-08-18 22:02:31 -07:00
Daniel Han
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>
2026-08-18 02:23:46 -07:00
Daniel Han
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>
2026-08-16 21:46:14 -07:00
Daniel Han
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 cfee13795 (before #8932),
fails on main with the TDZ error above, and passes again with this one-line
change. Typecheck clean, 2756 frontend tests pass.

#8932's own branch was already red with this exact failure before it merged.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Security: fix the filesystem-enumeration false positives in scan-packages

The hf-stack shard of pip scan-packages has been red on main. One CRITICAL
was blocking it, and tracking it down turned up a pattern bug behind nine of
the entries already in the baseline.

The blocking finding, unsloth-zoo/llama_cpp.py under "Harvests environment
variables/secrets AND makes network calls", is first-party: _github_auth_headers()
reads GH_TOKEN so the llama.cpp releases API call is not rate limited. That
file and check were already reviewed and baselined. It reopened because
unsloth-zoo 2026.8.12 replaced

    keynames = "\n" + "\n".join(os.environ.keys())

with a targeted _is_colab_environment() helper. Every other matched line is
byte identical to what was reviewed, so the current code is a strict subset of
the approved evidence. The entry is refreshed, with the hash generated by
--write-baseline rather than by hand. No PR caused this; it is the baseline's
reopen-on-change working as designed against an upstream release.

The pattern bug is in RE_FS_ENUM:

    r"|\bhistory\b.*\bread\b"  # reading shell history

Under re.DOTALL that .* spans the whole file, so any module containing the
word "history" anywhere before the word "read" anywhere is filesystem
enumeration, and with a network call in the same file that is a CRITICAL.
That is httpx's Response.history, retries.history in urllib3, IPython's
history module, torch's CUDA memory history. Nine of the eleven baselined
CRITICALs under this check were that one alternative, each suppressing a whole
file for the check.

Meanwhile the precise half was dead. \b\.bash_history\b puts \b between "/"
and "." in "~/.bash_history", where neither side is a word character, so it
could never match; same for \b\.zsh_history\b. This is the unsatisfiable-\b
bug already fixed once for /proc/self/status. Checked against the old pattern:
it matched none of five real history-file reads and all four benign cases.

Naming the files instead inverts that. The nine dead baseline entries are
removed, which narrows the allowlist rather than widening it, and three tests
pin both directions.

Verified by running all three shards locally against the same requirements
transform CI uses: hf-stack now exits 0 (was 1), studio and extras stay at 0,
and no removed entry resurfaced.

* Fix two more stale sidebar contract tests left by #8932

Both fail on pristine origin/main, so they are not from this branch. #8932
moved the sidebar's user-visible copy into the locale file and rewrote the
delete-switch predicate to cover its new bulk targets. Neither change breaks a
contract; both broke a grep of app-sidebar.tsx.

test_the_delete_switch_does_not_promise_project_files greps for the sentence
"This chat's own sandbox folder is removed from disk." It is still there,
verbatim, in studio/frontend/src/i18n/locales/en.ts. The promise is the
contract, not its address, so the test now searches the frontend sources and
survives the next move while still failing on a reworded promise.

test_the_delete_switch_reaches_a_chat_moved_into_a_project pinned one spelling
of deleteTargetHasFiles:

    -return target.kind === "project" || target.kind === "chat";
    +return target.kind !== "run";

Same answer for a chat and for a project, plus the new "chats" / "projects"
bulk kinds. What must hold is that a run is excluded and that project
membership is never consulted, so the test reads the brace-matched function
body and asserts that instead of the old one-liner.

Both were checked by mutation: reinstating the misleading copy, and gating
deleteTargetHasFiles on target.item.projectId, each fail the rewritten test.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address the review: scope a contract test, keep a parity check, close a scanner gap

Four items, all confirmed against the code at head before changing anything.

1. tests/studio/test_model_picker_contracts.py: the rollback ordering assertion
searched the whole of chat-page.tsx, which takes the same snapshot in the hub
auto-load path at line 2558. The only applyModelLoadConfigToRuntime call is at
3262, so the index comparison was satisfied by the unrelated occurrence: deleting
the snapshot inside selectWithConfig outright still passed both assertions.
Verified by mutation. Now scoped to the selectWithConfig body, which fails on
that deletion.

2. studio/backend/tests/test_research_internal_call_tool_gate.py: dropping
perf_callback from the kwargs comparison hid presence as well as identity, so the
opt-out losing its callback on one path would have gone unnoticed and cost that
path its tok/s readout. Assert both are callable (or both absent) first, then
exclude. Verified by mutation at routes/inference.py:14512.

3. tests/studio/test_model_picker_contracts.py was source-only and ran without the
backend environment; calling the real ladder for a standalone .gguf pulled in
hub.utils.gguf, then loggers, then structlog, so a bare pytest run failed after
183 passes. CI installs studio.txt and is unaffected, so coverage there is
unchanged. The helper now skips on a missing third-party package only; a missing
first-party module still fails.

4. scripts/scan_packages.py: fish stores history at $XDG_DATA_HOME/fish/fish_history
with no leading dot (fishshell.com/docs/current/cmds/history.html), so the dotted
alternative could never match the real path and narrowing the pattern left an
exfiltration blind spot. Added a non-dotted form. The fish read plus a network call
is a CRITICAL again, the dotted shells still match, and the false positives stay
suppressed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten comments added by the CI repair

* Fix two more stale studio contract constants left by #8932

Both fail on pristine origin/main, so neither comes from this branch, and in
both the source is right and the test-side constant is stale.

INLINE_ROW_IDS in tests/studio/playwright_mac_tab_capabilities.py still listed
four rows. #7863 had put Video under "More" as layout v5, so the script dropped
it: an unpinned row renders no data-testid and every assertion on it silently
observes nothing. #8932 pins Video under Images again, and records it in the
migration history as v7, so it is deliberate and versioned rather than an
accidental revert. Video is observable again, so the script samples it again.
test_inline_row_ids_match_the_frontends_default_pinned_set exists precisely to
catch this tuple drifting from the store in either direction, and it did its job.

test_multi_chat_prompt_queue_contract.py pinned the zero-argument spelling
"return await clearStoredChats();". #8932 gave the call an options argument,
which changes nothing about the ordering the assertion is there to hold, so it
now matches on the call prefix, the same way the sibling assertion three lines
up already does.

Both mutation-checked: unpinning Video in the store fails the first, and moving
requestPromptQueueStop after clearStoredChats fails the second.

* Narrow a contract search I made too wide, and read it once

Self-audit of an earlier fix in this branch, under the same standard applied to
everything else here.

test_the_delete_switch_does_not_promise_project_files greps for a promise the
delete dialog must make. #8932 moved that copy into the locale file, so the
original grep of app-sidebar.tsx broke, and my fix widened the search to the
whole frontend tree. That is the same defect the review caught in the rollback
ordering assertion: a search wide enough to be satisfied by an unrelated
occurrence proves nothing. The sentence appearing in any of ~1200 files, a
comment or a dead module included, would have passed it.

Now scoped by intent. The promise the dialog MUST make is looked for where the
sidebar's user-visible copy lives, the locales and the component. The promise it
must NOT make is still looked for across all of src, since breadth only makes a
negative stricter.

Mutation-checked: rewording the real string and planting the original in an
unrelated module fails the test, where the whole-tree form passed.

The two scopes are also read through an lru_cache. The wide one concatenates
about 1200 files, and it was re-reading every one of them on each call.

* Address the second review: real scoping, not the appearance of it

Five items, each reproduced against the code at head before changing anything.
Three are cases where my own earlier fix looked scoped but was not.

deleteTargetHasFiles: the negative assertions did not establish the contract.
`return target.kind === "run";` -- the exact inversion, which hides the delete
switch for every chat and project -- mentions "run", mentions no projectId, and
contains neither prohibited expression, so it passed all four checks. Confirmed
by construction. Now the direction is pinned: run is the kind excluded, never
the one included.

The selectWithConfig slice ran to end of file, not to the callback's closing
brace: 13,566 characters rather than 579. Moving applyModelLoadConfigToRuntime
out of the callback while leaving the snapshot behind still passed. Brace-matched
now, and that mutation fails.

asgi_stream_helpers returned on the frame without inspecting the task, so a
send() that sets the event and then raises left both futures done, the frame
branch won, and the caller's gather(return_exceptions = True) discarded the
exception. That is the silence the helper exists to break. Reproduced directly.
The task is checked first now, and the message says whether the failure came
before or after the frame.

Two scanner gaps, both from narrowing RE_FS_ENUM. Constructed fish paths put a
quote rather than a separator before the basename, so Path.home() / "fish" /
"fish_history" and os.path.join(h, "fish", "fish_history") did not match. And the
dotted list omitted PowerShell's ConsoleHost_history.txt, Ruby's .irb_history and
SQLite's .sqlite_history. A quote now counts as a boundary and those names are
covered, case-insensitively for the Windows one. Ten read forms match, and the
httpx, urllib3, IPython and torch false positives stay suppressed.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-16 02:02:47 -07:00
Wasim Yousef Said
83e163f13c
Revert "Desktop: ship a complete Linux AppImage (#8695)" (#8823)
This reverts commit 93a77d4104.
2026-08-14 06:55:54 -07:00
Wasim Yousef Said
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>
2026-08-14 03:46:47 -07:00
Daniel Han
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>
2026-08-12 09:55:26 -07:00
Daniel Han
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>
2026-08-12 06:50:34 -07:00
Daniel Han
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>
2026-08-12 05:36:17 -07:00
Daniel Han
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>
2026-08-11 06:03:28 -07:00
Daniel Han
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>
2026-08-10 20:47:54 -07:00
Daniel Han
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
2026-08-10 20:47:31 -07:00
Daniel Han
de12348a66
Stop writing the desktop build provenance section onto the release (#8340) 2026-08-10 06:01:11 -07:00
Wasim Yousef Said
3bbed688a8
CI: skip Windows release cache saves (#8325) 2026-08-10 02:36:24 -07:00
Wasim Yousef Said
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>
2026-08-10 01:01:39 -07:00
Daniel Han
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>
2026-08-09 04:48:38 -07:00
Daniel Han
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>
2026-08-09 03:03:00 -07:00
Daniel Han
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>
2026-08-09 01:52:12 -07:00
oobabooga
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>
2026-08-08 13:38:11 +02:00
Daniel Han
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>
2026-08-08 03:19:52 -07:00
oobabooga
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>
2026-08-07 05:27:54 -07:00
Daniel Han
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.
2026-08-07 03:41:51 -07:00
Daniel Han
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>
2026-08-07 03:27:08 -07:00
Daniel Han
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>
2026-08-07 03:15:48 -07:00
Daniel Han
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>
2026-08-07 02:22:37 -07:00
oobabooga
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>
2026-08-06 06:15:12 -07:00
Wasim Yousef Said
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>
2026-08-05 01:14:48 -07:00
Wasim Yousef Said
d8a30a98ff
Retry macOS disk image stapling (#7787) 2026-08-04 09:23:47 +02:00
oobabooga
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>
2026-07-31 04:43:54 +02:00
Leo Borcherding
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>
2026-07-26 23:31:56 -07:00
Wasim Yousef Said
26faceecf7
Harden desktop release token permissions (#7172)
* Harden desktop release token permissions

* Specify UTF-8 for workflow permission tests
2026-07-16 05:01:48 -07:00
Daniel Han
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>
2026-07-01 04:03:59 -07:00
Daniel Han
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>
2026-06-18 06:50:16 -07:00
Parvesh Saini
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>
2026-06-18 06:00:07 -07:00
Daniel Han
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>
2026-06-18 01:07:09 -07:00
Daniel Han
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>
2026-06-16 01:46:15 -07:00
Daniel Han
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.
2026-06-08 23:09:51 -07:00
Daniel Han
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.
2026-06-08 04:24:13 -07:00
Daniel Han
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>
2026-05-19 05:56:56 -07:00
Daniel Han
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>
2026-05-13 04:58:12 -07:00