diff --git a/.gitattributes b/.gitattributes index 6edd8950..110e050e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ # Bundled templates, scripts and sources are hashed, embedded and run as # written: keep LF line endings in every checkout, Windows included. * text=auto eol=lf +# Unified diffs require a leading context space before upstream tabs/blank lines. +tools/busybox-w32-patches/*.patch whitespace=-space-before-tab,-blank-at-eol diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index 220d2ff0..486ab143 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -29,6 +29,12 @@ jobs: timeout-minutes: 120 env: RUSTUP_TOOLCHAIN: '1.95.0' + # Full debug info plus incremental objects exhaust the hosted runner's + # disk before the integration binaries link. Keep line-number backtraces + # and every test; these jobs do not reuse incremental build state. + CARGO_PROFILE_DEV_DEBUG: line-tables-only + CARGO_PROFILE_TEST_DEBUG: line-tables-only + CARGO_INCREMENTAL: '0' SHADOWCODE_BUILD_JOBS: '4' # The Debian package must pass lintian (errors fail; warnings are reported). SHADOWCODE_REQUIRE_LINTIAN: '1' diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index 221a4fff..13bf8a75 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -24,6 +24,8 @@ on: - 'scripts/build-busybox-w32.sh' - 'scripts/build-llama.cpp.sh' - 'scripts/install-windows.ps1' + - 'scripts/stage-windows-crt.ps1' + - 'scripts/test-windows-installed-runtime.ps1' - 'packaging/windows/**' - 'src-tauri/tauri.windows.conf.json' - 'tools/busybox-w32.pin' @@ -98,6 +100,9 @@ jobs: path: packaging/busybox-w32/bin - name: Build the local model runtime (llama.cpp, CPU) run: bash scripts/build-llama.cpp.sh --no-user-install --cpu-only + - name: Stage app-local Microsoft CRT + shell: powershell + run: ./scripts/stage-windows-crt.ps1 - name: Build the installer (NSIS, current user) env: SHADOWCODE_COMMIT: ${{ github.sha }} @@ -128,6 +133,9 @@ jobs: profile="$(mktemp -d)" timeout 120 "$app/shadowcode.exe" --profile "$(cygpath -w "$profile")" doctor --json > "$profile/doctor.json" || true node -e "const d=require(process.argv[1]); const c=(d.checks||[]).find(x=>x.id==='appcontainer'); if(!c||c.status!=='pass') throw new Error('Windows sandbox check: '+JSON.stringify(c))" "$(cygpath -w "$profile/doctor.json")" + - name: Check installed binaries without toolchain DLLs on PATH + shell: powershell + run: ./scripts/test-windows-installed-runtime.ps1 -AppDirectory "$env:LOCALAPPDATA\ShadowCode" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: windows-installer diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e8e76bd..8fe92ce5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,12 @@ jobs: timeout-minutes: 150 env: RUSTUP_TOOLCHAIN: '1.95.0' + # Full debug info plus incremental objects exhaust the hosted runner's + # disk before the integration binaries link. Keep line-number backtraces + # and every test; these jobs do not reuse incremental build state. + CARGO_PROFILE_DEV_DEBUG: line-tables-only + CARGO_PROFILE_TEST_DEBUG: line-tables-only + CARGO_INCREMENTAL: '0' SHADOWCODE_BUILD_JOBS: '4' # The Debian package must pass lintian (errors fail; warnings are reported). SHADOWCODE_REQUIRE_LINTIAN: '1' @@ -145,6 +151,12 @@ jobs: timeout-minutes: 180 env: RUSTUP_TOOLCHAIN: '1.95.0' + # Full debug info plus incremental objects exhaust the hosted runner's + # disk before the integration binaries link. Keep line-number backtraces + # and every test; these jobs do not reuse incremental build state. + CARGO_PROFILE_DEV_DEBUG: line-tables-only + CARGO_PROFILE_TEST_DEBUG: line-tables-only + CARGO_INCREMENTAL: '0' SHADOWCODE_BUILD_JOBS: '4' SHADOWCODE_REQUIRE_LINTIAN: '1' outputs: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b9a6f73b..e0c13c36 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -8,9 +8,9 @@ name: Windows on: push: branches: [main] - paths: ['native/**', 'src-tauri/**', 'ui/**', 'Cargo.toml', 'Cargo.lock', '.github/workflows/windows.yml', 'scripts/build-llama.cpp.sh', 'tools/llama.cpp.pin', 'scripts/build-busybox-w32.sh', 'tools/busybox-w32.pin', 'packaging/windows/**'] + paths: ['native/**', 'src-tauri/**', 'ui/**', 'Cargo.toml', 'Cargo.lock', '.github/workflows/windows.yml', 'scripts/build-llama.cpp.sh', 'scripts/stage-windows-crt.ps1', 'scripts/test-windows-installed-runtime.ps1', 'tools/llama.cpp.pin', 'scripts/build-busybox-w32.sh', 'tools/busybox-w32.pin', 'tools/busybox-w32-patches/**', 'packaging/windows/**'] pull_request: - paths: ['native/**', 'src-tauri/**', 'ui/**', 'Cargo.toml', 'Cargo.lock', '.github/workflows/windows.yml', 'scripts/build-llama.cpp.sh', 'tools/llama.cpp.pin', 'scripts/build-busybox-w32.sh', 'tools/busybox-w32.pin', 'packaging/windows/**'] + paths: ['native/**', 'src-tauri/**', 'ui/**', 'Cargo.toml', 'Cargo.lock', '.github/workflows/windows.yml', 'scripts/build-llama.cpp.sh', 'scripts/stage-windows-crt.ps1', 'scripts/test-windows-installed-runtime.ps1', 'tools/llama.cpp.pin', 'scripts/build-busybox-w32.sh', 'tools/busybox-w32.pin', 'tools/busybox-w32-patches/**', 'packaging/windows/**'] permissions: contents: read concurrency: @@ -58,6 +58,11 @@ jobs: # The desktop crate embeds the built interface (tauri.conf.json frontendDist). - run: npm --prefix ui ci - run: npm --prefix ui run build + - name: Windows editor shortcuts and native clipboard regression tests + working-directory: ui + run: | + npx playwright install chromium + npx playwright test e2e/editor.spec.ts e2e/markdownWorker.spec.ts - name: Compile everything for Windows (all errors at once) run: | set -o pipefail @@ -144,6 +149,9 @@ jobs: ls packaging/llama.cpp/bin packaging/llama.cpp/bin/llama-server.exe --version packaging/llama.cpp/bin/llama-server.exe --list-devices + - name: Stage app-local Microsoft CRT + shell: powershell + run: ./scripts/stage-windows-crt.ps1 - name: Build the preview installer (NSIS, current user) run: CI=true ui/node_modules/.bin/tauri build --bundles nsis --config packaging/windows/bundle-runtime.conf.json - name: The app starts and reports its version @@ -162,6 +170,16 @@ jobs: ! grep -q "HOME is not set" <<<"$out" test -d "$(cygpath -u "$LOCALAPPDATA")/ShadowCode/state" test -d "$(cygpath -u "$APPDATA")/ShadowCode/config" + - name: Install and check with a clean runtime search path + shell: powershell + run: | + $setup = Get-ChildItem target/release/bundle/nsis/*-setup.exe | Select-Object -First 1 + if (!$setup) { throw 'Preview installer missing' } + $install = Start-Process $setup.FullName -ArgumentList '/S' -PassThru + $handle = $install.Handle + $install.WaitForExit() + if ($install.ExitCode -ne 0) { throw "Installer failed: $($install.ExitCode)" } + ./scripts/test-windows-installed-runtime.ps1 -AppDirectory "$env:LOCALAPPDATA\ShadowCode" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: windows-preview-installer diff --git a/.gitignore b/.gitignore index bc7ce666..bcecb5bf 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ src/shadow_agent/_assets/ /.claude/ /tools/busybox-w32/ /packaging/busybox-w32/bin/ +/packaging/windows/crt/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e6948b..d3c88d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ read-only toolchains readable, the rest of the home folder hidden; the network is off (no capabilities, this computer's own services included) or on. Commands run in a bundled busybox-w32 `sh`, built from a pinned - commit with one patch (the container may not open the null device, so - `/dev/null` uses a handle the launcher opens for it); its complete source + commit with patches for the sandbox null-device handle and waiting for + foreground commands to finish inside AppContainer; its complete source is attached to each release as `ShadowCode_VERSION_busybox-w32-source.tar.gz`. The network allow-list is refused on Windows. `shadowcode doctor` and **Settings › Permissions & network** report the Windows sandbox. @@ -31,6 +31,15 @@ - **Contested writes.** Settings files replaced by two writers at the same moment are retried (Windows refuses the second replacement), and writers in one process queue for a database's write lock instead of polling it. +- **Windows startup and command results.** Bundle the required Microsoft CRT + beside the app and managed runtime, remove the runtime's unused OpenSSL + dependency, and preserve sandboxed child output and exit status. Debug + desktop builds reserve enough stack for the async CLI. +- **Windows editing and worktrees.** Support both Ctrl+Y and Ctrl+Shift+Z + redo, and use the platform shell when setting up a worktree. +- **Local model selection.** Selecting a configured GGUF model from a fresh + CLI process no longer requires opening the picker first; stale catalog + entries cannot select files removed from the profile's configuration. - **Native runtime pins** follow Alpine 3.21's libcurl and libexpat security updates. diff --git a/README.md b/README.md index dd222bc2..7bcf7a93 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,17 @@ The code is open source; the Shadowfetch and ShadowCode product identity is cove own terminal, go from commit to pull request, preview the app you're building, dictate with your voice, and follow along from your phone. -ShadowCode **2.2.0** is the current release, available in [GitHub releases](https://github.com/ShadowfetchStudio/ShadowCode/releases/tag/v2.2.0) for x86_64 and 64-bit ARM Linux, from the signed APT repository below and for macOS (Apple silicon and Intel). -See the [2.2.0 release notes](https://github.com/ShadowfetchStudio/ShadowCode/releases/tag/v2.2.0) for changes and qualification -limits, and the [release history](CHANGELOG.md). Windows is in development -for the next release and is not released yet (see [Windows](#windows)). +ShadowCode **2.2.0** is the current public release for Linux and macOS, +available from [GitHub releases](https://github.com/ShadowfetchStudio/ShadowCode/releases/tag/v2.2.0) +and [shadowcode.org](https://shadowcode.org/). + +**2.3.0 is a release candidate**, adding Windows. Its Windows installers +have passed CI and physical-laptop qualification, but public publication +is still pending. See the [candidate release notes](docs/RELEASE_NOTES.md), +[Windows qualification record](docs/WINDOWS_LAPTOP_QUALIFICATION.md), and +[release history](CHANGELOG.md). Download instructions below use published +versions; the Windows section describes the forthcoming release. + ## Install @@ -162,7 +169,7 @@ On Windows the app is in `%LOCALAPPDATA%\ShadowCode`, settings in Shell commands run in an AppContainer sandbox with busybox `sh`, so they keep their POSIX syntax; the network allow-list is not available on Windows. The Windows version is new: it is tested on GitHub's Windows -runners, not yet on a wide range of PCs. See +runners and one Windows 11 laptop, not yet on a wide range of PCs. See [the Windows port notes](docs/WINDOWS_PORT.md). ### AppImage (recommended) diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index e8691838..b0be1358 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -24,10 +24,25 @@ ShadowCode 2.3.0 runs on Windows. The 2.2.0 notes follow below. on Windows with "Access is denied", and busy databases no longer time out with "database is locked". +- The Windows installer includes the Microsoft runtime DLLs needed to start + the app and local models on a machine without development tools. +- Sandboxed commands now wait for the child process and return its actual + output and exit status instead of sometimes reporting empty success. +- Windows redo accepts both Ctrl+Y and Ctrl+Shift+Z, and worktree setup uses + the platform shell. +- Selecting a local GGUF model from a fresh CLI process works without first + opening the model picker. + ## Known limits - The Windows version is new and has been tested on GitHub's Windows - machines, not yet on a wide range of PCs. Subscription tools were tested + machines and one Windows 11 laptop, not yet on a wide range of PCs. + Both the optimized Windows preview and release-workflow installer + completed a small Python repair task with Gemma 4 E4B on CPU, with the + original tests preserved and independently passing. This is not broad + model-quality qualification; see the + [qualification record](WINDOWS_LAPTOP_QUALIFICATION.md). + Subscription tools were tested there with stand-in programs, not yet with each vendor's own Windows CLI. Local models use the CPU only on Windows for now. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 44651b98..836bb5bf 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -6,8 +6,9 @@ and authentication receipts to the exact candidate before publication. Pushing a `v*` tag starts `.github/workflows/release.yml`. Publication follows build/qualification, protected signing review and verification of all seven -public assets. The signing environment permits only the tag being released -(`v2.3.0` for 2.3.0); every later version needs reviewed trust-policy, +public assets. The signing environment must permit the tag being released. As verified +on October 31, 2026, it still permits `v2.2.0`; before 2.3.0, change its +deployment rule to `v2.3.0` after candidate review. Every later version needs reviewed trust-policy, deployment-rule and tooling-pin updates. The workflow uses `docs/RELEASE_NOTES.md` as the release text. @@ -191,11 +192,14 @@ Public trust in `release/trust` currently allows the x86_64 and aarch64 targets, The `release-signing` environment has the signing secret and requires review by `Shadowfetchapps` (User ID `209457103`). Self-review is allowed and GitHub's default administrator override remains unchanged. For 2.3.0 its sole -deployment rule is the tag `v2.3.0` (it was `v2.2.0` for 2.2.0, `v2.1.0` for 2.1.0, `v1.0.1` for -1.0.1, `v1.0.0` for 1.0.0, `v0.34.2` for 0.34.2 and `v0.33.1` for 0.33.1); -`NATIVE_RELEASE_SIGNING_ENVIRONMENT` points to that environment. For 2.3.0, -`NATIVE_RELEASE_TOOLING_COMMIT` pins the tagged release commit itself, as it -did for 0.33.1 (`e15c4480e65db5650af012bb2a9773dbe89acf84`). +deployment rule must be changed to `v2.3.0`. As verified on October 31, +2026, the actual GitHub rule still permits only `v2.2.0` and +`NATIVE_RELEASE_TOOLING_COMMIT` still points to the 2.2.0 commit, +`444e75f13c3512d57c226b39d6edf59fdb08a4c6`. +`NATIVE_RELEASE_SIGNING_ENVIRONMENT` points to `release-signing`. For 2.3.0, +set the tooling pin to the reviewed tagged release commit itself, as was +done for 0.33.1 (`e15c4480e65db5650af012bb2a9773dbe89acf84`). +These are release prerequisites, not configuration already applied. Before tagging, finish review and push the tooling commit, then set `NATIVE_RELEASE_TOOLING_COMMIT` to that exact 40-character SHA. Read back this pin from GitHub configuration before dispatching the release. Verify the environment protections and source/candidate diff --git a/docs/WINDOWS_LAPTOP_QUALIFICATION.md b/docs/WINDOWS_LAPTOP_QUALIFICATION.md new file mode 100644 index 00000000..df7b22d7 --- /dev/null +++ b/docs/WINDOWS_LAPTOP_QUALIFICATION.md @@ -0,0 +1,218 @@ +# Windows laptop qualification — October 31, 2026 + +Test machine: Windows 11 Home x64, Ryzen 3 5300U, 32 GB RAM. Baseline: +`9c82cae76d4c94a4416405c93f00e198080a18ac`, Windows CI preview 2.3.0. +Tests use disposable profiles and projects; no vendor credentials are needed. + +## Reproduced defects and fixes + +- The original installed desktop executable exited with `0xC0000135` on + the machine before the Microsoft C++ runtime was installed. Stage the + redistributable x64 CRT from Visual Studio into both the desktop and + managed-runtime directories, and include it in the NSIS resource map. +- The original managed `llama-server.exe` also exited with `0xC0000135`: + its imports included OpenSSL DLLs absent from the package. At the pinned + llama.cpp revision, `LLAMA_CURL=OFF` is obsolete. Set `LLAMA_OPENSSL=OFF`; + ShadowCode downloads model files and uses loopback HTTP for inference. +- Worktree setup launched `sh` by name. A normal Git for Windows install + exposes `git` on PATH, not necessarily `sh`. Use the existing platform + shell selection, keeping the setup job's cancellation and process-tree + cleanup intact. +- CodeMirror's Windows default redo is Ctrl+Y. Add Ctrl+Shift+Z as an + additional binding, keeping Ctrl+Y. The live editor tests exercise both. +- The E2E build used a POSIX environment assignment that fails in Windows' + default npm shell. A small Node launcher now sets the build environment. +- Windows Clippy rejected a dummy unit-valued Unix endpoint cleanup guard. + Bind and retain that guard only on Unix; Windows continues to release + named pipes through their handles. +- The unoptimized installed desktop passed `--version`, but `run` exited + with stack overflow (`0xC00000FD`) before producing an event. Reserve an + 8 MiB main-thread stack for the MSVC desktop target. The installed-runtime + check now also calls `status` with an isolated profile, exercising the + async CLI rather than only the argument-parser version fast path. + +- Busybox-w32's final external-command path treated an inaccessible parent + process as a dead parent inside AppContainer. It returned zero before + Python finished; normal job cleanup then stopped Python, leaving an empty + tool receipt. Preserve foreground waiting when the launcher supplies its + sandbox null-device handle. A regression failed with the original binary + (expected exit 7, received exit 0 and empty output) and passed with the + patched pinned binary. Keep process-tree cleanup and confinement intact. +- Describe the actual platform sandbox in the exec schema and give explicit + workspace-relative cwd guidance. Invalid cwd errors now explain how to + use the project root without weakening path validation. + +- A fresh CLI process could not select a configured local GGUF model until + a picker/catalog read had populated the process-global cache. Selection + now scans the current profile's configured files before resolving the ID, + and refuses models removed from that configuration even if cached. + +## Regression coverage + +`test-windows-installed-runtime.ps1` checks the installed desktop, +`llama-server`, and `llama-cli` with a clean working directory and only +Windows system directories on PATH. It requires the app-local CRT files +and fails on a missing executable, nonzero exit, timeout, or missing version. +Both Windows packaging workflows run it after installation. + +Windows CI also runs the editor and Markdown clipboard browser tests. +Windows converts clipboard LF to CRLF; the assertions normalize that OS +representation while retaining full text, blank-line, and content checks. +The worktree unit fixture pins its own Git line-ending configuration, and +the absolute reset-time test uses a UTC reference rather than local noon. +The fake Codex server also opens its error log from Python rather than +through `cmd.exe` redirection. The latter locked the log during overlapping +server shutdown/startup and caused resumed-session tests to exit before +the next fake server could start. This is a test-helper fix, not a change +to vendor protocol or usage accounting. + +## Validation record + +- UI unit suite: 870 passed. Changed editor component: 20 passed. +- Browser baseline: 131 passed, five failed. After fixes, all 10 tests in + the affected editor/Markdown suites passed, including all five failures. +- Native unit suite after fixes: 607 passed, six intentionally ignored, + zero failed, using two test threads on the laptop. +- Integration run: 92 suites, 480 passed, three failed, six ignored. + The two missing-make failures passed in the five-case approval rerun. + The fake-server log-lock failure passed after the helper correction; + all 13 suites sharing that helper were rerun: 45 passed, zero failed, + one ignored. Raw failing runs were retained, not relabeled as passes. +- Windows sandbox and native-tool regression suites after the shell fix: + 22 passed, zero failed. +- Installed preview native WebView: window opened, fresh-profile + onboarding completed in an empty project, New task and Settings loaded, + with no observed page JavaScript errors. The final installed development + build also opened New task and Settings without observed JavaScript errors. + The temporary debugging task and its window were removed after the check. +- Workspace/all-target compilation with warnings denied: passed. +- Clippy across the workspace and all targets with warnings denied: passed. +- Rust formatting and secret scan: passed. +- Installed development build and optimized GitHub preview live-model + results: see below. Each artifact has its own qualification record; + development-build results do not certify a release artifact. + +The integration environment needs Git's Unix command-line utilities, +Python, Node and make, as on the Windows CI runner. CI also neutralizes +`core.autocrlf`; the laptop run temporarily applies the same setting and +restores the prior global value afterward. Test executables must not be +rebuilt while another run is executing them: Windows holds them open. + +## Local model + +Granite 4.2 3B, Q4_K_M, CPU backend, downloaded from the project's pinned +catalog. File size: 2,244,011,552 bytes. Verified SHA-256: +`e0406663965846ae22a403456eb826ccce5f450840491f71952f18a7cb78e7d5`. + +The first coding run at an explicitly reduced 8,192-token context timed out +after 15 minutes without repairing the fixture. It ran during compilation +and repeatedly compacted history. This is a failed qualification attempt, +not evidence that the model completed the task. Subsequent runs used the +normal 16,384-token context after builds/tests finished. + +A subsequent 16k run repaired the code and passed the independent five-test +check, but exhausted its 50k task-token budget while trying to verify the +result. The empty Python receipts led to the BusyBox defect above; this +attempt is not counted as a completed agent task. The post-fix run started +again from the original broken implementation. It preserved the original +tests and produced code passing all five independent tests, but timed out +after 900 seconds without completing the agent task. This also does not +qualify Granite as a recommended default on this laptop. + +A Gemma 4 E4B Q4_0 QAT comparison uses the same original task and tests, +16,384-token context, a 100k task-token budget and a 1,800-second timeout. +Its 5,154,941,280-byte file was verified against the pinned catalog digest: +`676c35070db6dbe52f93e9c864ee0fba4eddea94b9c875d9cb10daff453fbaee`. +The comparison completed in 622.01 seconds with agent exit 0 and independent +verifier exit 0. The original five tests were unchanged. The agent's actual +sandboxed `python3 -m unittest -q` receipt contains `Ran 5 tests` and `OK`, +and the independent verifier also passed all five. A separate evaluator +checked mixed hashable/unhashable equality, input preservation, clamp bounds, +and chunk size/flattening properties (1,653 assertions), all passing. + +This is evidence for a small Python repair task on this CPU, not a claim of +broad model reliability or cloud-agent qualification. Gemma is the preferred +local model from this comparison; Granite's unsuccessful attempts remain in +the record. + +The live task is the repository's fixed `clamp`, `unique`, and `chunks` +Python repair fixture. Its original five tests are preserved, and the +resulting implementation is tested independently with an evaluator-owned +copy of those tests. Runtime startup alone is not a coding-quality pass. + +## GitHub validation + +PR #11 starts independent Windows, macOS and Linux builds. The first Linux +x86-64 desktop job failed before tests because full Rust debug information +and incremental objects exhausted the hosted runner disk (`os error 28`). +The Linux verification and release jobs now use line-table debug information +and disable incremental state; all existing gates remain enabled. The +current rerun has passed workspace tests on both architectures; remaining +packaging checks are tracked below. + +The first Linux ARM run reached the integration tests and found a context +budget failure. A later x86-64 run exposed the same cause in four dedicated +4,096-token tester-route regressions: the expanded exec description raised +the short request to 4,113 tokens. The initially enlarged pressure fixture +was insufficient as a fix. The exec description is now concise while keeping +platform sandbox and relative-cwd guidance, and the pressure test is restored +to 4,096 tokens with explicit compaction/reduction/response-budget assertions. +Production context-budget enforcement and all tester-route limits remain +unchanged. On Windows, all five tester-route tests and the restored 4,096-token +pressure regression pass with the final schema, as does workspace/all-target +Clippy with warnings denied. The subsequent Linux workspace suites passed +on both architectures; their remaining packaging checks still need to finish. + +The cold-process model-selection regression was reproduced with the prior +installed binary and passed with the corrected development installer. A +second selection after removing the configured file correctly failed, even +with the model previously cached. The normal laptop profile now selects the +verified Gemma model with a 16,384-token context. + +## Optimized GitHub installer qualification + +Source commit: `b5ecd8d87ad23f963bc569416dc8c589069003e1`. +Windows build, engine unit/integration tests and installer checks passed in +[run 41219098028](https://github.com/ShadowfetchStudio/ShadowCode/actions/runs/41219098028). +The separate release build passed in +[run 41219097933](https://github.com/ShadowfetchStudio/ShadowCode/actions/runs/41219097933). +These are PR artifacts, not published release downloads. + +The preview installer's SHA-256 is +`7c077227703f05941289ac24816080847224c58f67474814e73fa144d989ff44`. +Its installed executable's SHA-256 is +`b996ff85a5feb96b07b761b34b4d1989ac4d8a4713cade2abf66e136dc144f9f`. +Clean-PATH startup passed. Native WebView checks passed replace-all, undo, +Ctrl+Shift+Z and Ctrl+Y redo, disk-verified save, and local-model settings, +with no observed page errors. + +A fresh Gemma run on this preview completed in 613.60 seconds, with agent +exit 0 and independent verifier exit 0. All five original tests were +unchanged and passed. The agent executed the test command inside +AppContainer and returned the actual test output. It initially repeated +reads and attempted unavailable Git-based delegation in the non-Git +fixture, then recovered and completed the task. Those detours remain in +the event record; this does not establish broad agent reliability. +The earlier 1,653-property check applies to the development run only. + +The release-workflow installer's SHA-256 is +`edbf891802fc7bc962665c3d845cba73cba3ec2d1b114a87f5d9a40737b93ee9`. +Its separate installation, clean-PATH startup and native WebView editor +checks passed. Its installed executable's SHA-256 is +`598b56f182eb95e78d956ef238628144b9d1cdb5b11415bfc213094dad8368d3`. +Its fresh Gemma run completed in 582.62 seconds with agent exit 0, +independent verifier exit 0, and all five original tests preserved and +passing. The agent's sandboxed test command also returned all five tests +passing. This run took unnecessary search/read turns before editing; it +qualifies this bounded fixture, not broad model reliability. A final +published installer must match qualified bytes or undergo qualification +again. + +Both Linux architectures have passed workspace tests and the 100-task +native-window endurance check at this commit. The ARM packaging container +then failed because Alpine replaced exact package revisions. The build pins +have been refreshed against both architecture indexes, and retained musl +and zlib source records now match their actual package revisions, including +all distribution patches verified against APKBUILD SHA-512 values. Full +packaging qualification must pass on the refreshed inputs. Both Apple +silicon and Intel macOS builds passed in run 41219097984. diff --git a/docs/WINDOWS_PORT.md b/docs/WINDOWS_PORT.md index 621ebd7f..1761a659 100644 --- a/docs/WINDOWS_PORT.md +++ b/docs/WINDOWS_PORT.md @@ -1,12 +1,12 @@ # Windows port -Status: phase 1 (compile on Windows) done October 17, 2026; phase 2 -(engine tests) started. Linux and -macOS ship today; Windows follows the path the macOS port took -([MACOS_PORT_ESTIMATE.md](MACOS_PORT_ESTIMATE.md)): compile, then make the -engine's tests pass, then the app, then packaging. Work happens on GitHub's -Windows runners (`.github/workflows/windows.yml`); no Windows computer is -needed for phases 1 and 2. +Status: Windows 2.3.0 release candidate, qualified on GitHub Windows runners +and a Windows 11 Home x64 laptop (Ryzen 3 5300U, 32 GB RAM). Compilation, +engine tests, installer startup, native editor operations and a local-model +repair task have passed. Public release publication remains pending; a CI +artifact is not a public release. See +[the laptop qualification record](WINDOWS_LAPTOP_QUALIFICATION.md) for exact +artifact evidence, failed attempts and remaining limitations. ## Phases @@ -15,7 +15,7 @@ needed for phases 1 and 2. 2. **Engine tests.** Unit and integration tests run on the Windows runner; Unix-only tests are ported or gated with a reason. 3. **App.** The window, terminals, local models and the agent's tools work on - Windows; checked on a Windows virtual machine. + Windows; checked on Windows CI and a physical Windows 11 laptop. 4. **Packaging.** An installer (`.msi` or `.exe`) built by a release workflow and attached to each release like the macOS images and the Flatpak. @@ -30,8 +30,8 @@ needed for phases 1 and 2. | Engine connection | Unix socket | Named pipe restricted to the user | | API keys | Secret Service, Keychain | Windows Credential Manager | | Settings and history | XDG folders, `~/Library` | Settings in `%APPDATA%\ShadowCode\config` (they roam with the account); history, models and caches in `%LOCALAPPDATA%\ShadowCode` | -| Local models | Vulkan, Metal | Vulkan and CPU builds of llama.cpp | -| Installer and updates | AppImage, deb, Flatpak, dmg | `.msi`/`.exe` from Tauri; the update notice names it | +| Local models | Vulkan, Metal | Bundled CPU build of llama.cpp; Windows GPU acceleration is not qualified for this release | +| Installer and updates | AppImage, deb, Flatpak, dmg | Per-user NSIS `.exe` from Tauri, with app-local Microsoft runtime DLLs; the update notice names it | ## Progress @@ -101,4 +101,9 @@ needed for phases 1 and 2. checks that the app reports its version, and checks that it keeps its data in `%LOCALAPPDATA%` and `%APPDATA%`. It is a 14-day CI artifact, not a release. -- Not yet tried: the app on a real Windows PC (only CI runners so far). +- Physical-laptop qualification (October 31): installed preview startup, + native WebView editor save/undo/redo, model settings and a complete Gemma + 4 E4B CPU repair task passed. The separate release-workflow installer also + passed startup, native editor checks and a fresh local-model repair task; + its exact artifact qualification is recorded separately. Real vendor-account + sessions and broad Windows hardware coverage remain unqualified. diff --git a/native/core/examples/support/coding_fixture.rs b/native/core/examples/support/coding_fixture.rs index f59e616e..b8e04e71 100644 --- a/native/core/examples/support/coding_fixture.rs +++ b/native/core/examples/support/coding_fixture.rs @@ -75,10 +75,17 @@ print('SHADOWCODE_EVALUATOR:5:PASS' if passed else 'SHADOWCODE_EVALUATOR:FAIL') sys.exit(0 if passed else 1) "#, )?; + // Windows has no /usr/bin. Use the same PATH interpreter as the task's + // check; the independently owned grader and isolated mode stay unchanged. + let interpreter = if cfg!(windows) { + "python3 -I grade.py" + } else { + "/usr/bin/python3 -I grade.py" + }; let prepared = sandbox::prepare_shell( verifier.path(), verifier.path(), - "/usr/bin/python3 -I grade.py", + interpreter, &ShellPolicy { network: ShellNetwork::Off, allow: Vec::new(), diff --git a/native/core/src/control.rs b/native/core/src/control.rs index b0280990..baaf83b1 100644 --- a/native/core/src/control.rs +++ b/native/core/src/control.rs @@ -405,7 +405,7 @@ impl Server { let (mut listener, lease) = bind(&endpoint)?; // A pipe disappears with its last handle; there is nothing to sweep. #[cfg(windows)] - let (mut listener, lease) = (transport::Listener::bind(&endpoint.path)?, ()); + let mut listener = transport::Listener::bind(&endpoint.path)?; let state = Arc::new(State { cancel: CancellationToken::new(), finished: AtomicBool::new(false), @@ -422,6 +422,7 @@ impl Server { let profile = endpoint.profile.clone(); tokio::spawn(async move { let _completion = Completion(running.clone()); + #[cfg(unix)] let _lease = lease; let mut clients = JoinSet::new(); let owners = Arc::new(Semaphore::new(8)); diff --git a/native/core/src/resume.rs b/native/core/src/resume.rs index f4c3e983..08f982c4 100644 --- a/native/core/src/resume.rs +++ b/native/core/src/resume.rs @@ -433,7 +433,9 @@ mod tests { Some(local(2026, 10, 1, 15, 40)) ); assert_eq!( - from_text("quota resets at 2026-09-29T18:00:00Z", now), + // This absolute timestamp must be in the future regardless of the + // machine's local time zone (noon Pacific is already past 18 UTC). + from_text("quota resets at 2026-09-29T18:00:00Z", 1_790_701_200.0), chrono::DateTime::parse_from_rfc3339("2026-09-29T18:00:00Z") .ok() .map(|t| t.timestamp() as f64) diff --git a/native/core/src/service/model_catalog.rs b/native/core/src/service/model_catalog.rs index dad016e3..4b63c64b 100644 --- a/native/core/src/service/model_catalog.rs +++ b/native/core/src/service/model_catalog.rs @@ -168,6 +168,13 @@ impl Service { let model = if !call.text("provider").is_empty() { self.model_from_body(&call.body, &cfg.model) } else { + // A fresh CLI process has not opened the picker. Resolve + // only files in this profile's current catalog, rather + // than requiring (or trusting) a process-global cache. + if body.id.as_str().starts_with("local:gguf:") { + crate::local_engine::entry_for_id(&cfg.local_engine, body.id.as_str()) + .context("That local model is not in the configured catalog")?; + } self.resolve_model(body.id.as_str(), &cfg.model)? }; self.check_model_identity(&model)?; diff --git a/native/core/src/tools.rs b/native/core/src/tools.rs index d9bda4c0..a8a30bd0 100644 --- a/native/core/src/tools.rs +++ b/native/core/src/tools.rs @@ -1822,6 +1822,12 @@ pub fn schemas_tiered(tier: DescriptionTier) -> Vec { let s = json!({"type":"string"}); let n = json!({"type":"integer"}); let b = json!({"type":"boolean"}); + // Small-context routes need a genuinely short schema; parameter descriptions + // are not truncated by the tiering below. Keep cwd guidance in both tiers. + let exec_description = match tier { + DescriptionTier::Short => "Approved shell; cwd '.' = workspace root.", + DescriptionTier::Full => "Run a shell command after approval in the platform sandbox when available (empty home, writable project, network per settings). Omit cwd or use '.' for the project root; otherwise use a workspace-relative subdirectory. Changed files are checkpointed for rewind. Output and runtime are bounded.", + }; let specs=vec![ ("system_info","Inspect OS and connected displays. Read-only.",json!({}),vec![]), ("list_files","List direct children of a workspace directory.",json!({"path":s,"max_entries":n}),vec![]), @@ -1846,7 +1852,7 @@ pub fn schemas_tiered(tier: DescriptionTier) -> Vec { ("create_directory","Create a workspace directory and parents.",json!({"path":s}),vec!["path"]), ("move_file","Move a regular file without overwriting its destination; checkpoint both paths.",json!({"src":s,"dest":s}),vec!["src","dest"]), ("delete_file","Delete one regular workspace file after approval; retain a checkpoint.",json!({"path":s,"expected_hash":s}),vec!["path"]), - ("exec","Run a shell command after approval, in the bubblewrap sandbox when available (empty home, writable project, network per settings). Project files it changes are checkpointed for rewind. Output and runtime are bounded.",json!({"command":s,"cwd":s,"timeout_sec":n}),vec!["command"]), + ("exec",exec_description,json!({"command":s,"cwd":s,"timeout_sec":n}),vec!["command"]), ("git_status","Show repository status.",json!({}),vec![]), ("git_diff","Show staged or unstaged changes. External diff helpers and textconv are disabled.",json!({"staged":b}),vec![]), ("git_log","Show recent commits.",json!({"limit":n}),vec![]), diff --git a/native/core/src/tools/shell.rs b/native/core/src/tools/shell.rs index 82bae3f3..685e52b0 100644 --- a/native/core/src/tools/shell.rs +++ b/native/core/src/tools/shell.rs @@ -20,7 +20,9 @@ impl ToolExecutor { )? .min(self.config.agent.tool_timeout_sec as usize); let cwd = if let Some(path) = args["cwd"].as_str() { - let relative = self.workspace.relative(path)?; + let relative = self.workspace.relative(path).context( + "Invalid exec cwd: use '.' for the project root or a workspace-relative subdirectory", + )?; let cwd = self.workspace.path.join(relative).canonical()?; ensure!( cwd.starts_with(&self.workspace.path), diff --git a/native/core/src/worktree_tasks.rs b/native/core/src/worktree_tasks.rs index 3bf410c5..598078ff 100644 --- a/native/core/src/worktree_tasks.rs +++ b/native/core/src/worktree_tasks.rs @@ -1123,6 +1123,9 @@ mod tests { let source = root.path().join("source"); fs::create_dir(&source).unwrap(); command(&source, &["init", "-q"]); + // These tests assert exact file bytes after a checkout; isolate them + // from the developer's global Windows line-ending preference. + command(&source, &["config", "core.autocrlf", "false"]); fs::write(source.join("tracked.txt"), b"source bytes\n").unwrap(); command(&source, &["add", "tracked.txt"]); command(&source, &["commit", "-qm", "Base"]); diff --git a/native/core/src/worktree_tasks/setup.rs b/native/core/src/worktree_tasks/setup.rs index ddf5f239..45e3ece0 100644 --- a/native/core/src/worktree_tasks/setup.rs +++ b/native/core/src/worktree_tasks/setup.rs @@ -212,9 +212,10 @@ async fn run_command( if cancel.is_cancelled() { return json!({"command": command, "ok": false, "stopped": "cancelled", "seconds": 0.0, "output": "Not started: ShadowCode is closing"}); } - let mut process = tokio::process::Command::new("sh"); + let shell = crate::process::ProcessSpec::shell(command, dir.to_path_buf(), timeout); + let mut process = tokio::process::Command::new(&shell.program); process - .args(["-c", command]) + .args(&shell.args) .current_dir(dir) .envs(env) .env("CI", "1") diff --git a/native/core/tests/autonomy_021.rs b/native/core/tests/autonomy_021.rs index 1759664a..77fbc724 100644 --- a/native/core/tests/autonomy_021.rs +++ b/native/core/tests/autonomy_021.rs @@ -529,10 +529,20 @@ fn repeated_compact_under_context_pressure_keeps_the_task() { messages.push(json!({"role":"assistant","content":"Decision: we will use the tester route."})); messages.push(json!({"role":"user","content":"Continue feature X and verify it"})); let schemas = shadowcode_core::tools::schemas(); + // Keep small-context models supported as the tool schema changes. + // The generated history exceeds this window: prove compaction runs. + let context_limit = 4096; + let before = context::estimate_tokens(&json!(messages)); + let mut compacted = false; for _ in 0..8 { - let _ = context::compact(&mut messages, &schemas, 4096, 0.55).unwrap(); + compacted |= context::compact(&mut messages, &schemas, context_limit, 0.55) + .unwrap() + .is_some(); context::validate_pairs(&messages).unwrap(); } + assert!(compacted, "fixture must exercise context pressure"); + assert!(context::estimate_tokens(&json!(messages)) < before); + context::response_budget(&messages, &schemas, context_limit).unwrap(); let tape = serde_json::to_string(&messages).unwrap(); assert!( tape.contains("feature X") || tape.contains("never overwrite"), @@ -542,7 +552,7 @@ fn repeated_compact_under_context_pressure_keeps_the_task() { messages.last().unwrap()["content"], "Continue feature X and verify it" ); - let budget = autonomy::account(&messages, &schemas, 4096).unwrap(); + let budget = autonomy::account(&messages, &schemas, context_limit).unwrap(); eprintln!( "context_pressure fits={} used={} remaining={}", budget["fits"], budget["used_estimated_tokens"], budget["remaining"] diff --git a/native/core/tests/local_engine.rs b/native/core/tests/local_engine.rs index ea9cf123..d6b64949 100644 --- a/native/core/tests/local_engine.rs +++ b/native/core/tests/local_engine.rs @@ -792,6 +792,37 @@ async fn hermes_incomplete_calls_and_prose_xml_never_execute_tools() { } } +#[tokio::test] +async fn selecting_local_model_refreshes_cold_catalog_and_rejects_removed_files() { + let f = fixture(GPU); + let path = f.models.join("cold-selection.gguf"); + qwen_like(&path, "qwen3", TOOLS_TEMPLATE); + let id = local_engine::entry_id(&path); + Config::patch(&f.paths, json!({"local_engine":{"files":[path]}})).unwrap(); + let service = Service::open(f.paths.clone(), Some(f.project.clone())).unwrap(); + assert!(local_engine::known(&id).is_none()); + let selected = call(&service, "POST", "/api/models/select", json!({"id":id})) + .await + .unwrap(); + assert_eq!(selected["model"]["default"], id); + assert_eq!(selected["model"]["provider"], "llamacpp"); + assert_eq!(Config::load(&f.paths, None).unwrap().model.default, id); + assert!(lines(&f.bin.join("launches.jsonl")).is_empty()); + + // A stale entry in the process-global cache cannot resurrect a model + // removed from this profile's configured files. + Config::patch(&f.paths, json!({"local_engine":{"files":[]}})).unwrap(); + assert!(local_engine::known(&id).is_some()); + let error = call(&service, "POST", "/api/models/select", json!({"id":id})) + .await + .unwrap_err(); + assert!( + error.to_string().contains("configured catalog"), + "{error:#}" + ); + service.engine.shutdown().await.unwrap(); +} + #[tokio::test] async fn catalog_routes_add_list_remove_picker_and_weights_stay() { let f = fixture(GPU); diff --git a/native/core/tests/native_tools.rs b/native/core/tests/native_tools.rs index 7849cba9..a1cefbc7 100644 --- a/native/core/tests/native_tools.rs +++ b/native/core/tests/native_tools.rs @@ -52,6 +52,37 @@ async fn call(tools: &ToolExecutor, name: &str, args: Value) -> shadowcode_core: .unwrap() } +#[tokio::test] +async fn exec_cwd_guidance_preserves_workspace_boundary_and_root_execution() { + let mut config = Config::default(); + config.permissions.approve_shell = false; + // Even a preapproved command cannot escape the workspace cwd boundary. + config.permissions.require_approval_for_dangerous = false; + let (root, tools) = fixture(config); + let denied = call( + &tools, + "exec", + json!({"command":"printf escaped > must-not-exist", "cwd":".."}), + ) + .await; + assert!(!denied.success); + assert!( + denied.error.contains("use '.' for the project root"), + "{}", + denied.error + ); + assert!(!root.path().join("must-not-exist").exists()); + assert!(!tools.workspace.path.join("must-not-exist").exists()); + let ran = call( + &tools, + "exec", + json!({"command":"printf root-ok", "cwd":"."}), + ) + .await; + assert!(ran.success, "{}", ran.error); + assert_eq!(ran.output["stdout"], "root-ok"); +} + #[tokio::test] async fn system_info_is_read_only_and_cannot_read_an_arbitrary_path() { let mut config = Config::default(); diff --git a/native/core/tests/vendor_support/mod.rs b/native/core/tests/vendor_support/mod.rs index 23a4fe37..548000a4 100644 --- a/native/core/tests/vendor_support/mod.rs +++ b/native/core/tests/vendor_support/mod.rs @@ -15,6 +15,10 @@ use std::{ pub const FAKE_CODEX: &str = r#"#!/usr/bin/env python3 import json, os, sys, time HERE = os.path.dirname(os.path.abspath(__file__)) +if os.name == "nt": + # cmd.exe redirection exclusively locks the log while the old server + # exits, preventing a resumed turn from starting its next fake server. + sys.stderr = open(os.path.join(HERE, "stderr.log"), "a", encoding="utf-8") def cfg(): try: with open(os.path.join(HERE, "fake.json")) as f: @@ -299,10 +303,7 @@ impl FakeCodex { .expect("python must be on PATH to run the fake Codex"); fs::write( dir.join("codex.cmd"), - format!( - "@\"{}\" \"%~dp0codex.py\" %* 2>>\"%~dp0stderr.log\"\r\n", - python.display() - ), + format!("@\"{}\" \"%~dp0codex.py\" %*\r\n", python.display()), ) .unwrap(); } diff --git a/native/core/tests/vendor_usage.rs b/native/core/tests/vendor_usage.rs index 2f2881ad..5dada0ea 100644 --- a/native/core/tests/vendor_usage.rs +++ b/native/core/tests/vendor_usage.rs @@ -206,7 +206,12 @@ async fn resumed_codex_turns_count_each_turn_once() { ) .await; let second = finished(service, second["id"].as_str().unwrap()).await; - assert_eq!(second["status"], "completed", "{second}"); + assert_eq!( + second["status"], + "completed", + "{second}; fake stderr: {:?}", + setup.fake.marker("stderr.log") + ); let threads = setup.fake.marker("threads.log").unwrap(); assert!( threads diff --git a/native/core/tests/windows_sandbox.rs b/native/core/tests/windows_sandbox.rs index e9a401e3..55b22944 100644 --- a/native/core/tests/windows_sandbox.rs +++ b/native/core/tests/windows_sandbox.rs @@ -87,6 +87,48 @@ fn dunce_path(path: &Path) -> std::path::PathBuf { std::path::PathBuf::from(text.strip_prefix(r"\\?\").unwrap_or(&text)) } +#[test] +fn foreground_external_commands_keep_their_output_and_exit_code_in_a_job() { + let _serial = SERIAL + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + std::env::set_var( + "SHADOWCODE_SANDBOX_LAUNCHER", + env!("CARGO_BIN_EXE_shadowcode-sandbox"), + ); + let root = tempfile::tempdir().unwrap(); + let workspace = dunce_path(&root.path().canonicalize().unwrap()); + let runtime = tokio::runtime::Runtime::new().unwrap(); + for code in [7, 0] { + // No trailing shell builtin: exercise busybox's final-command exec + // path under the same job cleanup used by the real agent. + let command = format!("cmd.exe /c \"echo external-output & exit /b {code}\""); + let prepared = + sandbox::prepare_shell(&workspace, &workspace, &command, &policy(true)).unwrap(); + let result = runtime + .block_on(shadowcode_core::process::run( + shadowcode_core::process::ProcessSpec { + program: prepared.program, + args: prepared.args, + cwd: workspace.clone(), + timeout: std::time::Duration::from_secs(15), + output_limit: 4096, + env: Default::default(), + child: prepared.child, + }, + tokio_util::sync::CancellationToken::new(), + None, + )) + .unwrap(); + assert_eq!(result.exit_code, code, "{result:?}"); + assert_eq!(result.ok, code == 0, "{result:?}"); + assert!(result.stdout.contains("external-output"), "{result:?}"); + if let Some(scratch) = prepared.scratch { + sandbox::discard_scratch(&scratch).unwrap(); + } + } +} + /// Programs start in the container (a failed start is 0xC0000142): system /// programs and busybox `sh`, the sandbox's shell. Git for Windows' bash /// (MSYS2) cannot start in an AppContainer; it is not used inside it. The diff --git a/packaging/native-runtime/alpine-sources.json b/packaging/native-runtime/alpine-sources.json index b9f09a3c..5110a537 100644 --- a/packaging/native-runtime/alpine-sources.json +++ b/packaging/native-runtime/alpine-sources.json @@ -254,8 +254,8 @@ }, { "name": "musl", - "version": "1.2.5-r11", - "aportsCommit": "21cafa1183cf7501d3f1744994d221c038436e45", + "version": "1.2.5-r12", + "aportsCommit": "5245849e14d213abe18d589b4b80d65793327940", "license": "MIT", "archive": "musl-1.2.5.tar.gz", "sourceDirectory": "musl-1.2.5", @@ -265,8 +265,8 @@ "files": [ { "file": "APKBUILD", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/APKBUILD", - "sha512": "c14d5f5d0557aa15b3f377ab336be85ba2efc0d560860ae19536907a100018cd7caaac1c866079025cae8d1fc5bc799da23b21120c30d743baeaf6df2e718819" + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/APKBUILD", + "sha512": "bfc98bac42d6d48c7ce55e4e47ec658f85466a9907fdf4541f98d2285e451dce9aa839ca3cea156c3e2a7f9bd270bbdbe95c6ad453cdcaeff9c3b027c3145f3b" }, { "file": "musl-1.2.5.tar.gz", @@ -275,85 +275,90 @@ }, { "file": "handle-aux-at_base.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/handle-aux-at_base.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/handle-aux-at_base.patch", "sha512": "a76f79b801497ad994746cf82bb6eaf86f9e1ae646e6819fbae8532a7f4eee53a96ac1d4e789ec8f66aea2a68027b0597f7a579b3369e01258da8accfce41370" }, { "file": "isatty-glibc-align.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/isatty-glibc-align.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/isatty-glibc-align.patch", "sha512": "4fe4309d9e5eb12f3aaf9dac300c7bf1a9e104d9b0e7754353314f4b6507de68d8d5bf0ac45164d7e2ab4d71728b5ef8b302f8ee7a6b548f54e022218031e2cf" }, { "file": "ldso-fix-non-functional-fix-to-early-dynamic-PAGE_SI.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/ldso-fix-non-functional-fix-to-early-dynamic-PAGE_SI.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/ldso-fix-non-functional-fix-to-early-dynamic-PAGE_SI.patch", "sha512": "1b85079c706b017106ecbf4afbc65b4a2171297a673035645e4c3ddb7e2aae0549b04932df83778323b7329ae8d32160a89e0c1b2534c278708c61b1f40a1ed8" }, { "file": "loongarch64-Add-lsx-and-lasx-vector-structure-defini.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/loongarch64-Add-lsx-and-lasx-vector-structure-defini.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/loongarch64-Add-lsx-and-lasx-vector-structure-defini.patch", "sha512": "b4d95231944fd236827a62343bbce74b20a56739e2b8937bda69bfc7b53f2ce5bd709459ee61615bd10d2839f43311eced9344ee8185701fd38ec0de0d43c6ae" }, { "file": "loongarch64-add-TLSDESC-support.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/loongarch64-add-TLSDESC-support.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/loongarch64-add-TLSDESC-support.patch", "sha512": "363d0cbfe11f024b120b6d54e909c038cd5b37f1f195048ddb29591191a1288ff50842ccd14e0cab80359e5b46c0e02d2b5af3016bd1507c1d269e590690a3fe" }, { "file": "loongarch64-add-fpu-and-simd-context-to-signal.h.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/loongarch64-add-fpu-and-simd-context-to-signal.h.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/loongarch64-add-fpu-and-simd-context-to-signal.h.patch", "sha512": "b0f93f8868caf15d436ebe81088f4914cac3e21cc5b86cb0d5f40092edc6e29909188a14cde0c69ab86b51631aa34a4b8136a515f2a1b53c424c8b251c5ea689" }, { "file": "loongarch64-add-bits-hwcap.h.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/loongarch64-add-bits-hwcap.h.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/loongarch64-add-bits-hwcap.h.patch", "sha512": "70003d89e31b1c83274d1e850f86d8a54f30c1d5e22f769ae229d30d2bbdc095dbf4c262fb55079aaf8bfdf9a213f9468150b50c000b6596b051729a0d25f913" }, { "file": "CVE-2025-26519.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/CVE-2025-26519.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/CVE-2025-26519.patch", "sha512": "7a6a9836d2de91afc1115868e68f347bd2365fa48188e65938cfa654ae9bafdbb3a56bf12d3185a96800a85198378c8dbf9c25d977ca0e318220529fa4458123" }, { - "file": "iconv-gb18030-fix.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/iconv-gb18030-fix.patch", + "file": "CVE-2026-6042.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/CVE-2026-6042.patch", "sha512": "f7849abeab0e4eab992a80464afa07b9c8aae5cee76040523b9dbc0931435f28ea8f5792b8a4a0cd6d608ac2f37e3225e89afcb3171a0a6df44450ed57cee83b" }, { "file": "CVE-2026-40200.patch", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/CVE-2026-40200.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/CVE-2026-40200.patch", "sha512": "a64ab7688d1a85e560b5687783df482d2467a79a74400da5a1601382847d6b4e6a79b7529a8dc80c46ddda8dc2a812f2261557fc8ee98dc3bdf7322761bd6d9c" }, + { + "file": "CVE-2026-107891.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/CVE-2026-107891.patch", + "sha512": "df73a35b9da531021ad5b1a6aad12b9d8d69de2bed7fc91b1156d9727476ab25c0d3c2341d60488152ee8ccbddb83786e91fff950cdb177b9b04673da98fdc4e" + }, { "file": "ldconfig", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/ldconfig", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/ldconfig", "sha512": "8d3a2d5315fc56fee7da9abb8b89bb38c6046c33d154c10d168fb35bfde6b0cf9f13042a3bceee34daf091bc409d699223735dcf19f382eeee1f6be34154f26f" }, { "file": "__stack_chk_fail_local.c", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/__stack_chk_fail_local.c", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/__stack_chk_fail_local.c", "sha512": "062bb49fa54839010acd4af113e20f7263dde1c8a2ca359b5fb2661ef9ed9d84a0f7c3bc10c25dcfa10bb3c5a4874588dff636ac43d5dbb3d748d75400756d0b" }, { "file": "getconf.c", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/getconf.c", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/getconf.c", "sha512": "0d80f37b34a35e3d14b012257c50862dfeb9d2c81139ea2dfa101d981d093b009b9fa450ba27a708ac59377a48626971dfc58e20a3799084a65777a0c32cbc7d" }, { "file": "getent.c", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/getent.c", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/getent.c", "sha512": "27fa820dc847b1e4a89a45ab7c7ffb8b6436b6b7f1089004b12561d939b227617797d5fc81696658da3f07daab6cc71ea171eb6afb6f5082336b57e2d0ac8463" }, { "file": "iconv.c", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/21cafa1183cf7501d3f1744994d221c038436e45/main/musl/iconv.c", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/5245849e14d213abe18d589b4b80d65793327940/main/musl/iconv.c", "sha512": "9d42d66fb1facce2b85dad919be5be819ee290bd26ca2db00982b2f8e055a0196290a008711cbe2b18ec9eee8d2270e3b3a4692c5a1b807013baa5c2b70a2bbf" } ] }, { "name": "zlib", - "version": "1.3.2-r0", - "aportsCommit": "2b38f55109add14f4f99a974c2fdf421b6b9e9e9", + "version": "1.3.2-r1", + "aportsCommit": "c973d294927355eac1d2a6e97d86e41fbab8ffda", "license": "Zlib", "archive": "zlib-1.3.2.tar.gz", "sourceDirectory": "zlib-1.3.2", @@ -363,8 +368,8 @@ "files": [ { "file": "APKBUILD", - "url": "https://raw.githubusercontent.com/alpinelinux/aports/2b38f55109add14f4f99a974c2fdf421b6b9e9e9/main/zlib/APKBUILD", - "sha512": "056333150006abc2a800310022ce22e8a126bae7cf867f4441ec775a0a2d63d8f036adb6bb693bbe7b777c25a51d8f0a9aafc6517817d2df82413ee6558b40b3" + "url": "https://raw.githubusercontent.com/alpinelinux/aports/c973d294927355eac1d2a6e97d86e41fbab8ffda/main/zlib/APKBUILD", + "sha512": "c1f60be4e44bb3f38fb4b5d44bcb408ec6c3d035d63829668435989adc674dc292c48656a0adf081a3eebcd33fff1da75db329778d117b9f2db94a4176b82e48" }, { "file": "zlib-1.3.2.tar.gz", @@ -373,6 +378,11 @@ "https://distfiles.alpinelinux.org/distfiles/v3.21/zlib-1.3.2.tar.gz" ], "sha512": "70963771ea5d763614278a69b474f09b7d237ef8f53b675a10fe31d9923aeef601504b35d7ebd1b1e7f347e9ebb048e6b3b47fffdf137e7bdc7e8d5eb4ec4692" + }, + { + "file": "CVE-2026-85091.patch", + "url": "https://raw.githubusercontent.com/alpinelinux/aports/c973d294927355eac1d2a6e97d86e41fbab8ffda/main/zlib/CVE-2026-85091.patch", + "sha512": "878888308a51296470f05eca59a79212314d4c9a26c19bbf70c1cb1b8d25eb42e0cce5a001acbe287268aa215c854cb5a9990ff62b7aefa9de221803d172957e" } ] }, diff --git a/packaging/native-runtime/apk-packages.txt b/packaging/native-runtime/apk-packages.txt index 53c391a1..8246dfe8 100644 --- a/packaging/native-runtime/apk-packages.txt +++ b/packaging/native-runtime/apk-packages.txt @@ -9,9 +9,9 @@ automake=1.17-r0 bash=5.2.37-r0 binutils=2.43.1-r3 blkid=2.40.4-r1 -brotli-libs=1.1.0-r2 +brotli-libs=1.1.1-r0 build-base=0.5-r3 -c-ares=1.34.8-r0 +c-ares=1.34.9-r0 cfdisk=2.40.4-r1 clang19=19.1.4-r0 clang19-headers=19.1.4-r0 @@ -48,7 +48,6 @@ libcap-getcap=2.78-r0 libcap-ng=0.8.5-r0 libcap2=2.78-r0 libcurl=8.14.1-r3 -aarch64:libcurl=8.14.1-r2 libeconf=0.6.3-r0 libexpat=2.9.0-r0 libfdisk=2.40.4-r1 @@ -93,22 +92,22 @@ mount=2.40.4-r1 mpc1=1.3.1-r1 mpdecimal=4.0.0-r0 mpfr4=4.2.1-r0 -musl-dev=1.2.5-r11 +musl-dev=1.2.5-r12 ncurses-terminfo-base=6.5_p20241006-r3 nghttp2-libs=1.69.0-r0 oniguruma=6.9.9-r0 openssl=3.3.7-r2 partx=2.40.4-r1 patch=2.7.6-r10 -pcre2=10.43-r0 +pcre2=10.43-r1 perl=5.40.4-r0 perl-error=0.17029-r2 perl-git=2.47.3-r0 pkgconf=2.3.0-r0 -pyc=3.12.15-r0 -python3=3.12.15-r0 -python3-pyc=3.12.15-r0 -python3-pycache-pyc0=3.12.15-r0 +pyc=3.12.16-r0 +python3=3.12.16-r0 +python3-pyc=3.12.16-r0 +python3-pycache-pyc0=3.12.16-r0 readline=8.2.13-r0 runuser=2.40.4-r1 samurai=1.2-r6 diff --git a/packaging/windows/bundle-runtime.conf.json b/packaging/windows/bundle-runtime.conf.json index 3dfb307d..f940eb99 100644 --- a/packaging/windows/bundle-runtime.conf.json +++ b/packaging/windows/bundle-runtime.conf.json @@ -2,7 +2,8 @@ "bundle": { "resources": { "../packaging/llama.cpp/bin/": "llama.cpp/", - "../packaging/busybox-w32/bin/": "busybox/" + "../packaging/busybox-w32/bin/": "busybox/", + "../packaging/windows/crt/": "." } } } diff --git a/scripts/build-llama.cpp.sh b/scripts/build-llama.cpp.sh index cb639d0a..5465f29f 100755 --- a/scripts/build-llama.cpp.sh +++ b/scripts/build-llama.cpp.sh @@ -284,6 +284,9 @@ if [[ "$(uname -m)" == aarch64 && "$DARWIN" == "0" ]]; then COMPILER_FLAGS=(-DCMAKE_C_COMPILER="$LLAMA_CC" -DCMAKE_CXX_COMPILER="$LLAMA_CXX") fi +# The managed server is loopback HTTP only; ShadowCode handles downloads. +# LLAMA_CURL is deprecated at this pin and no longer disables OpenSSL. Leaving +# HTTPS enabled can bind Windows artifacts to unbundled CI-machine SSL DLLs. # ${array[@]+...}: empty arrays are "unbound" under set -u in bash 3.2 (macOS). "$CMAKE" -S "$SRC" -B "$BUILD_DIR" ${GENERATOR[@]+"${GENERATOR[@]}"} ${COMPILER_FLAGS[@]+"${COMPILER_FLAGS[@]}"} \ -DCMAKE_BUILD_TYPE=Release \ @@ -298,7 +301,7 @@ fi -DLLAMA_BUILD_EXAMPLES=OFF \ -DLLAMA_BUILD_TOOLS=ON \ -DLLAMA_BUILD_SERVER=ON \ - -DLLAMA_CURL=OFF \ + -DLLAMA_OPENSSL=OFF \ -DCMAKE_INSTALL_RPATH="$RPATH" \ -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON "$CMAKE" --build "$BUILD_DIR" --config Release --parallel "$JOBS" diff --git a/scripts/native-test-session.mjs b/scripts/native-test-session.mjs index e1ce3033..cc9c020a 100644 --- a/scripts/native-test-session.mjs +++ b/scripts/native-test-session.mjs @@ -260,7 +260,16 @@ export async function inspectPrivateActivations(session, { io = linuxProc, ignor } } } - throw error; + // Record only public process identity fields, never environment values. + // This distinguishes exit/PID races from an unreadable live helper in + // hosted-runner failures without changing ownership or signal rules. + const snapshot = value => value && Object.fromEntries( + ['pid', 'parent', 'start', 'state', 'uid'].map(key => [key, value[key]]), + ); + throw Object.assign(new Error(`${error.message}; private-session identity ${JSON.stringify({ + observed: snapshot(info), current: snapshot(current), daemon: snapshot(daemon), + daemon_ancestors: ancestors ? [...ancestors] : null, + })}`, { cause: error }), { code: error.code }); } if (!(env?.DBUS_SESSION_BUS_ADDRESS === address && env.DBUS_STARTER_ADDRESS === address && env.DBUS_STARTER_BUS_TYPE === 'session')) return; const value = await io.identity(pid); diff --git a/scripts/stage-windows-crt.ps1 b/scripts/stage-windows-crt.ps1 new file mode 100644 index 00000000..298e578b --- /dev/null +++ b/scripts/stage-windows-crt.ps1 @@ -0,0 +1,26 @@ +# Stage Microsoft's app-local redistributable CRT, not DLLs from System32. +# Both the desktop executable and the separately launched model runtime need it. +$ErrorActionPreference = 'Stop' +$root = Split-Path $PSScriptRoot -Parent +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +$vs = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (!$vs) { throw 'Visual Studio C++ build tools are required to stage the redistributable CRT.' } +$redist = Get-ChildItem (Join-Path $vs 'VC\Redist\MSVC') -Directory | + Where-Object { $_.Name -match '^\d+\.\d+\.\d+$' } | + Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1 +if (!$redist) { throw 'MSVC redistributable directory was not found.' } +$crt = Get-ChildItem (Join-Path $redist.FullName 'x64') -Directory -Filter 'Microsoft.VC*.CRT' | Select-Object -First 1 +if (!$crt) { throw 'The x64 Microsoft CRT redistributables were not found.' } +$files = @(Get-ChildItem $crt.FullName -File -Filter '*.dll') +foreach ($required in @('vcruntime140.dll','vcruntime140_1.dll','msvcp140.dll')) { + if ($files.Name -notcontains $required) { throw "Missing redistributable: $required" } +} +$dest = Join-Path $root 'packaging\windows\crt' +$runtime = Join-Path $root 'packaging\llama.cpp\bin' +if (!(Test-Path (Join-Path $runtime 'llama-server.exe'))) { throw 'Build the managed runtime before staging its CRT.' } +New-Item -ItemType Directory -Force $dest | Out-Null +foreach ($file in $files) { + Copy-Item $file.FullName $dest -Force + Copy-Item $file.FullName $runtime -Force +} +Write-Output "Staged x64 CRT $($redist.Name) for the desktop and managed runtime." diff --git a/scripts/test-native-test-session.mjs b/scripts/test-native-test-session.mjs index 2511d0a4..eb4f989b 100644 --- a/scripts/test-native-test-session.mjs +++ b/scripts/test-native-test-session.mjs @@ -162,6 +162,21 @@ function unreadable(f, pid) { throw Object.assign(new Error('fixture environment denied'), { code: 'EACCES' }); }; } +test('unreadable activation diagnostics include identity but not environment or executable data', async () => { + const f = fixture(); const session = await capturePrivateSession({ address, io: f.io }); + f.rows.set(22, { ...f.proc(22, 2, 14, '/private/executable-path'), secret: 'private-fixture-value' }); + unreadable(f, 22); + const result = await session.cleanup(); + assert.equal(result.ok, false); + assert.deepEqual(f.signals, []); + const failure = result.failures.join(' '); + assert.match(failure, /"observed":\{"pid":22,"parent":2,"start":"14","state":"S","uid":1000\}/); + assert.match(failure, /"current":/); + assert.match(failure, /"daemon":/); + assert(!failure.includes('/private/executable-path')); + assert(!failure.includes('private-fixture-value')); + assert(!failure.includes(address)); +}); test('an unreadable live process in an older, unrelated branch is ignored and reported', async () => { const f = fixture(); const session = await capturePrivateSession({ address, io: f.io }); f.rows.set(20, f.proc(20, 0, 5, '/runner/worker')); diff --git a/scripts/test-windows-installed-runtime.ps1 b/scripts/test-windows-installed-runtime.ps1 new file mode 100644 index 00000000..2bfb1629 --- /dev/null +++ b/scripts/test-windows-installed-runtime.ps1 @@ -0,0 +1,51 @@ +param([Parameter(Mandatory=$true)][string]$AppDirectory) +$ErrorActionPreference = 'Stop' +$originalPath = $env:Path +$scratch = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory $scratch | Out-Null +try { + foreach ($directory in @($AppDirectory, (Join-Path $AppDirectory 'llama.cpp'))) { + foreach ($dll in @('vcruntime140.dll','vcruntime140_1.dll','msvcp140.dll')) { + if (!(Test-Path (Join-Path $directory $dll))) { throw "Missing app-local CRT: $directory\$dll" } + } + } + # Do not accidentally satisfy package dependencies from Git, OpenSSL, + # Python or a build toolchain installed on the qualification machine. + $env:Path = "$env:SystemRoot\System32;$env:SystemRoot" + foreach ($relative in @('shadowcode.exe', 'llama.cpp\llama-server.exe', 'llama.cpp\llama-cli.exe')) { + $exe = Join-Path $AppDirectory $relative + if (!(Test-Path $exe)) { throw "Missing installed executable: $relative" } + $stdout = Join-Path $scratch 'stdout.txt' + $stderr = Join-Path $scratch 'stderr.txt' + $process = Start-Process $exe -ArgumentList '--version' -PassThru -WorkingDirectory $scratch -RedirectStandardOutput $stdout -RedirectStandardError $stderr + $handle = $process.Handle # Keep the process handle alive for ExitCode on Windows PowerShell 5.1. + if (!$process.WaitForExit(60000)) { + Stop-Process -Id $process.Id -Force + throw "Version check timed out: $relative" + } + $process.Refresh() + $output = (Get-Content $stdout,$stderr -Raw -ErrorAction SilentlyContinue) -join "`n" + if ($process.ExitCode -ne 0) { throw "$relative exited $($process.ExitCode): $output" } + if ($output -notmatch '(?i)version|ShadowCode') { throw "$relative produced no version: $output" } + Write-Output "PASS $relative" + Write-Output $output + } + # --version exits during argument parsing. Exercise the async CLI too: + # a debug build can pass --version but overflow Windows' default stack. + $arguments = @('--profile', "`"$scratch\profile`"", '--workspace', "`"$scratch`"", 'status') + $process = Start-Process (Join-Path $AppDirectory 'shadowcode.exe') -ArgumentList $arguments -PassThru -WorkingDirectory $scratch -RedirectStandardOutput $stdout -RedirectStandardError $stderr + $handle = $process.Handle + if (!$process.WaitForExit(60000)) { + Stop-Process -Id $process.Id -Force + throw 'Installed CLI status timed out' + } + $process.Refresh() + if ($process.ExitCode -ne 0) { + $output = (Get-Content $stdout,$stderr -Raw -ErrorAction SilentlyContinue) -join "`n" + throw "Installed CLI status exited $($process.ExitCode): $output" + } + Write-Output 'PASS shadowcode.exe status (isolated profile)' +} finally { + $env:Path = $originalPath + Remove-Item $scratch -Recurse -Force +} diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e6..966ed9fd 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,11 @@ fn main() { + // Windows' default 1 MiB main-thread stack is too small for the CLI's + // unoptimized async entry point: `run` overflows before the first event. + // Reserve the usual Unix-sized stack; pages are committed on demand. + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") + && std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") + { + println!("cargo:rustc-link-arg-bin=shadowcode=/STACK:8388608"); + } tauri_build::build() } diff --git a/tools/busybox-w32-patches/0002-wait-for-exec-in-the-sandbox.patch b/tools/busybox-w32-patches/0002-wait-for-exec-in-the-sandbox.patch new file mode 100644 index 00000000..5930de72 --- /dev/null +++ b/tools/busybox-w32-patches/0002-wait-for-exec-in-the-sandbox.patch @@ -0,0 +1,17 @@ +diff --git a/win32/process.c b/win32/process.c +--- a/win32/process.c ++++ b/win32/process.c +@@ -493,7 +493,12 @@ + DWORD code; + int status; + +- if (getppid() == 1) ++ /* AppContainer process enumeration may hide our live parent. The ++ * ShadowCode launcher supplies the inherited null-device handle in ++ * this environment: do not detach an exec child in that sandbox. ++ * Its output and actual exit code belong to the foreground command. ++ */ ++ if (getppid() == 1 && !getenv("SHADOWCODE_NULL_HANDLE")) + exit(0); + + kill_child_ctrl_handler(GetProcessId(child)); diff --git a/ui/e2e/build.mjs b/ui/e2e/build.mjs new file mode 100644 index 00000000..07dffefc --- /dev/null +++ b/ui/e2e/build.mjs @@ -0,0 +1,23 @@ +import { execFileSync } from "node:child_process"; + +// Use Node directly: POSIX inline environment assignments fail in cmd.exe, +// which is npm's default script shell on Windows. +execFileSync( + process.execPath, + ["node_modules/typescript/bin/tsc", "--noEmit"], + { stdio: "inherit" }, +); +execFileSync( + process.execPath, + [ + "node_modules/vite/bin/vite.js", + "build", + "--outDir", + "dist-e2e", + "--emptyOutDir", + ], + { + stdio: "inherit", + env: { ...process.env, VITE_SHADOW_TEST_TRANSPORT: "1" }, + }, +); diff --git a/ui/e2e/editor.spec.ts b/ui/e2e/editor.spec.ts index c5c67e0b..c153cfa0 100644 --- a/ui/e2e/editor.spec.ts +++ b/ui/e2e/editor.spec.ts @@ -131,6 +131,12 @@ test("renders syntax, searches and replaces, then undoes and saves through the l await expectEditorText(editor, original); await editor.press("Control+Shift+z"); await expectEditorText(editor, replaced); + if (process.platform === "win32") { + await editor.press("Control+z"); + await expectEditorText(editor, original); + await editor.press("Control+y"); + await expectEditorText(editor, replaced); + } await editor.press("Control+s"); await expect.poll(() => savedFile(page, "sample.ts")).toBe(replaced); await expect(drawer.getByText("Saved", { exact: true })).toBeVisible(); diff --git a/ui/e2e/markdownWorker.spec.ts b/ui/e2e/markdownWorker.spec.ts index e4f93ef7..cec2f968 100644 --- a/ui/e2e/markdownWorker.spec.ts +++ b/ui/e2e/markdownWorker.spec.ts @@ -90,15 +90,22 @@ for (const [mode, csp] of [ await expect(answer).toContainText(""); await expect(answer).toContainText("© ≂̸"); await answer.getByRole("button", { name: "Copy code" }).click(); - expect(await page.evaluate(() => navigator.clipboard.readText())).toBe( - "const n = 1;\n\nconsole.log(n);\n", - ); + // Windows' native clipboard converts line endings to CRLF. + expect( + (await page.evaluate(() => navigator.clipboard.readText())).replace( + /\r\n/g, + "\n", + ), + ).toBe("const n = 1;\n\nconsole.log(n);\n"); await answer .getByRole("button", { name: "Copy answer", exact: true }) .click(); - expect(await page.evaluate(() => navigator.clipboard.readText())).toBe( - completed, - ); + expect( + (await page.evaluate(() => navigator.clipboard.readText())).replace( + /\r\n/g, + "\n", + ), + ).toBe(completed); const workers = await page.evaluate( () => (window as any).__SHADOW_MARKDOWN_WORKERS__, ); diff --git a/ui/package.json b/ui/package.json index 13f4f719..8318bb24 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,7 +9,7 @@ "build": "tsc --noEmit && vite build", "typecheck": "tsc --noEmit", "test": "vitest run", - "build:e2e": "tsc --noEmit && VITE_SHADOW_TEST_TRANSPORT=1 vite build --outDir dist-e2e --emptyOutDir", + "build:e2e": "node e2e/build.mjs", "check:bundle": "node e2e/check-bundle.mjs", "test:e2e": "node e2e/check-bundle.mjs && playwright test", "format": "prettier --write src e2e *.ts *.json" diff --git a/ui/src/components/CodeEditor.tsx b/ui/src/components/CodeEditor.tsx index 38bb5bf1..065cf276 100644 --- a/ui/src/components/CodeEditor.tsx +++ b/ui/src/components/CodeEditor.tsx @@ -7,7 +7,7 @@ import { type Extension, } from "@codemirror/state"; import { EditorView, keymap } from "@codemirror/view"; -import { indentWithTab } from "@codemirror/commands"; +import { indentWithTab, redo } from "@codemirror/commands"; import { HighlightStyle, indentUnit, @@ -151,6 +151,9 @@ export function CodeEditor(props: CodeEditorProps) { // Register our explicit save/exit policy first so Escape reaches it. Prec.highest( keymap.of([ + // CodeMirror's Windows default is Ctrl+Y. Keep that binding and + // also accept the cross-platform Shift+Mod+Z shortcut. + { key: "Mod-Shift-z", run: redo, preventDefault: true }, { key: "Mod-s", scope: "editor search-panel",