From 51710f8d656b53fec7c511938a725d98a873a64c Mon Sep 17 00:00:00 2001 From: Aron Bijl Date: Wed, 14 Oct 2026 13:16:34 +0200 Subject: [PATCH 1/9] [prepare] Reproduce downloaded Python.NET assembly startup failure on isolated Windows --- .github/workflows/runtime-diagnostics.yml | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/runtime-diagnostics.yml diff --git a/.github/workflows/runtime-diagnostics.yml b/.github/workflows/runtime-diagnostics.yml new file mode 100644 index 0000000..b29531f --- /dev/null +++ b/.github/workflows/runtime-diagnostics.yml @@ -0,0 +1,46 @@ +name: Diagnose downloaded Windows runtime +on: + push: + branches: [fix/windows-runtime-v0.7.1] + paths: ['.github/workflows/runtime-diagnostics.yml'] +permissions: + contents: read +jobs: + diagnostics: + runs-on: windows-latest + timeout-minutes: 8 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install "pywebview>=6,<7" + - name: Compare the same Python.Runtime.dll before and after an Internet-zone tag + shell: python + run: | + import importlib.metadata as md, json, pathlib, subprocess, sys + root=pathlib.Path('test-results/runtime-diagnostics'); root.mkdir(parents=True,exist_ok=True) + dist=md.distribution('pythonnet') + dll=pathlib.Path(dist.locate_file('pythonnet/runtime/Python.Runtime.dll')) + assert dll.is_file() + tag=pathlib.Path(str(dll)+':Zone.Identifier') + def probe(): + p=subprocess.run([sys.executable,'-c','import clr; print("Python.NET initialized")'],capture_output=True,text=True,timeout=45) + return {'returncode':p.returncode,'stdout':p.stdout,'stderr':p.stderr} + report={'versions':{name:md.version(name) for name in ('pywebview','pythonnet','clr_loader')},'plain':probe()} + try: + tag.write_text('[ZoneTransfer]\nZoneId=3\nHostUrl=https://github.com/AronAxe/GraphPaper/releases/\n',encoding='utf-8') + report['internet_marked']=probe() + finally: + tag.unlink(missing_ok=True) + report['unblocked']=probe() + report['reproduced']=report['plain']['returncode']==0 and report['internet_marked']['returncode']!=0 and 'Python.Runtime.Loader.Initialize' in report['internet_marked']['stderr'] and report['unblocked']['returncode']==0 + (root/'report.json').write_text(json.dumps(report,indent=2),encoding='utf-8') + print(json.dumps(report,indent=2)) + subprocess.run(['git','archive','--format=zip','HEAD','-o',str(root/'source.zip')],check=True) + - uses: actions/upload-artifact@v4 + if: always() + with: + name: runtime-investigation + path: test-results/runtime-diagnostics/ + retention-days: 3 From 39bbd932a48653ccef5d9e3efcf159eecba67a03 Mon Sep 17 00:00:00 2001 From: Aron Bijl Date: Wed, 14 Oct 2026 13:27:03 +0200 Subject: [PATCH 2/9] [prepare] Apply tested scoped Windows runtime startup repair --- .github/workflows/apply-runtime-fix.yml | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/apply-runtime-fix.yml diff --git a/.github/workflows/apply-runtime-fix.yml b/.github/workflows/apply-runtime-fix.yml new file mode 100644 index 0000000..45a7d4f --- /dev/null +++ b/.github/workflows/apply-runtime-fix.yml @@ -0,0 +1,54 @@ +name: Apply verified Windows runtime repair +on: + push: + branches: [fix/windows-runtime-v0.7.1] + paths: ['.github/workflows/apply-runtime-fix.yml'] +permissions: + contents: write +jobs: + apply: + runs-on: ubuntu-latest + timeout-minutes: 8 + steps: + - uses: actions/checkout@v4 + with: + ref: fix/windows-runtime-v0.7.1 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Apply exact tested patch without changing workflow permissions + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api repos/AronAxe/GraphPaper/git/blobs/076495d09cb43325e051fce5e120473353549af3 > /tmp/patch.json + python - <<'PY' + import base64,hashlib,json,lzma,subprocess + from pathlib import Path + raw=json.loads(Path('/tmp/patch.json').read_text()) + patch=lzma.decompress(base64.b64decode(raw['content'])) + assert hashlib.sha256(patch).hexdigest()=='ac2909c57f3ca5a956a2986ae6caeb27da78ee1703834b577935d405c7b596ff' + p=Path('/tmp/runtime.patch');p.write_bytes(patch) + subprocess.run(['git','apply','--check',str(p)],check=True) + subprocess.run(['git','apply',str(p)],check=True) + assert not subprocess.check_output(['git','diff','--name-only','--','.github/workflows']).strip() + PY + - run: python -m pip install -q -r requirements-dev.txt + - run: python -m pytest -q --junitxml=test-results/runtime-linux.xml + - run: python -m unittest discover -s docs/tests -p 'test_*.py' -v + - run: python scripts/docs_tool.py check + - name: Commit ordinary source only + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -A + python scripts/prepare_assets.py --manifest + git add publish-manifest.json + test -z "$(git diff --cached --name-only -- .github/workflows)" + git diff --cached --check + git commit -m 'Repair Internet-marked bundled DLLs with explicit consent and per-build checksums' + git push origin HEAD:fix/windows-runtime-v0.7.1 + - uses: actions/upload-artifact@v4 + if: always() + with: + name: runtime-repair-linux-tests + path: test-results/runtime-linux.xml From 34c987f08818c1ddf76f1a471ae37fcea3ae773d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:28:05 +0000 Subject: [PATCH 3/9] Repair Internet-marked bundled DLLs with explicit consent and per-build checksums --- CHANGELOG.md | 8 ++ README.md | 2 + docs/README.md | 2 +- docs/RELEASE-0.7.1.md | 17 +++ docs/SECURITY.md | 4 + docs/TROUBLESHOOTING.md | 4 + docs/WINDOWS.md | 8 ++ .../v0.7.1/download-mark-reproduction.json | 23 +++ docs/wiki-pages.json | 2 +- graphpaper/__init__.py | 2 +- graphpaper/_runtime_hashes.py | 2 + graphpaper/desktop.py | 8 +- graphpaper/windows_runtime.py | 132 ++++++++++++++++++ publish-manifest.json | 33 +++-- pyproject.toml | 2 +- scripts/build_release.ps1 | 8 +- scripts/check_downloaded_release.py | 41 ++++++ scripts/native_smoke.py | 35 ++++- scripts/runtime_manifest.py | 37 +++++ tests/test_windows_runtime.py | 85 +++++++++++ 20 files changed, 435 insertions(+), 20 deletions(-) create mode 100644 docs/RELEASE-0.7.1.md create mode 100644 docs/validation/v0.7.1/download-mark-reproduction.json create mode 100644 graphpaper/_runtime_hashes.py create mode 100644 graphpaper/windows_runtime.py create mode 100644 scripts/check_downloaded_release.py create mode 100644 scripts/runtime_manifest.py create mode 100644 tests/test_windows_runtime.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c52d3da..1203b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.7.1 — Downloaded Windows runtime fix + +- Detect Internet-blocked bundled window DLLs before Python.NET initialization. +- Verify per-build DLL hashes; ask native consent for narrowly scoped unblocking. +- Replace blanket WebView2 advice with error-specific diagnostics. +- Gate releases on fresh ZIP extraction and native consent/marked-DLL regression, including existing System One, graph and writing workflows. + + ## 0.6.0 - Replace the green wash with ink-blue/charcoal surfaces and a licensed scan of real graph paper in the sidebar. diff --git a/README.md b/README.md index 5f7a810..0906f82 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Use **Codex / ChatGPT sign-in** or your own model API. Keep control of the argum **The spatial graph studio.** Orbit or flatten your graph, focus its neighborhoods, and edit nodes and connections directly. Ink-blue surfaces and a real scanned graph-paper sidebar. [Explore the graph controls](docs/GRAPH-STUDIO.md). +**Windows startup fix in 0.7.1:** downloaded runtime files are checked against this build and unblocked only after your permission. The complete ZIP is now tested with simulated Internet download marks. [Startup help](docs/WINDOWS.md#downloaded-runtime-permission). + ## How GraphPaper works ![GraphPaper workflow: sources to graph, angles, outline, draft, review and export, guided by your brief and reusable voice with optional Science research and JEV decision support](docs/images/graphpaper-workflow.svg) diff --git a/docs/README.md b/docs/README.md index f6a11c9..0e2aab0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ **Connect your sources. Find your angle. Write in your own voice.** -These guides cover **GraphPaper 0.7.0**: the System One decision studio, ink-blue interface, real graph-paper sidebar, spatial graph editor, four writing modes and reusable voice graphs. [Download for Windows](https://github.com/AronAxe/GraphPaper/releases/latest) · [System One guide](SYSTEM-ONE.md) · [See the interface](VISUAL-TOUR.md) · [Version history](../CHANGELOG.md). +These guides cover **GraphPaper 0.7.1**: the System One decision studio, ink-blue interface, real graph-paper sidebar, spatial graph editor, four writing modes and reusable voice graphs. [Download for Windows](https://github.com/AronAxe/GraphPaper/releases/latest) · [System One guide](SYSTEM-ONE.md) · [See the interface](VISUAL-TOUR.md) · [Version history](../CHANGELOG.md). ## Start here diff --git a/docs/RELEASE-0.7.1.md b/docs/RELEASE-0.7.1.md new file mode 100644 index 0000000..8992759 --- /dev/null +++ b/docs/RELEASE-0.7.1.md @@ -0,0 +1,17 @@ +# GraphPaper 0.7.1 — Windows downloaded-runtime startup fix + +Addresses the reproduced **Failed to resolve Python.Runtime.Loader.Initialize** startup failure caused by Windows Internet-zone marks on bundled DLLs. The previous native tests exercised build files, not this downloaded-file condition. + +- Checks exact bundled DLL paths and embedded per-build hashes before loading the Python/.NET bridge. +- Shows **GraphPaper runtime permission** only when affected files carry an Internet/Restricted-zone mark. Choose Yes only if you trust your downloaded release; No leaves the marks in place and cancels startup. +- Removes only the listed runtime DLL marks, never project files, unrelated DLLs, or system-wide security policy. Incorrect or mixed DLLs are not unblocked. +- Gives error-specific startup advice rather than recommending WebView2 for every error. +- Tests a fresh extraction of the actual release ZIP with simulated DLL Internet-zone marks, both native consent choices, and the full native graph, Science, Polemic, voice and System One workflows. This does not simulate SmartScreen or all antivirus/policy configurations. + +## Install + +Extract the complete ZIP into a **new folder** and open **GraphPaper.exe** with `_internal` beside it. Approve the scoped runtime dialog when shown and when you trust the release. Keep the existing project data directory. No graph rebuild or voice retraining is needed. + +The System One decision studio from v0.7.0 is retained. This patch changes startup/distribution handling, not your writing policy, models, prompts or project data. All release tests use isolated GitHub-hosted machines and synthetic project/model fixtures, not a user's computer or private manuscripts. + +[Windows startup details](https://github.com/AronAxe/GraphPaper/blob/main/docs/WINDOWS.md#downloaded-runtime-permission) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index a3b8561..3a57851 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -56,3 +56,7 @@ The Windows release bundles an official Codex runtime with version and checksum Before sharing a log, screenshot or project, remove credentials, confidential writing and personal data. Report ordinary reproducible defects through [Issues](https://github.com/AronAxe/GraphPaper/issues). Use an established private contact route to the repository maintainer for a security-sensitive report; do not publish active credentials or an exploit against someone’s live installation. This documentation describes implemented safeguards and their limits, not a formal penetration-test certification. [Validation scope →](QUALITY.md) + +## Downloaded Windows runtime files + +The v0.7.1 frozen build embeds an allowlist of exact DLL paths and SHA-256 hashes for Python.NET, clr-loader and pywebview. The desktop startup checks files against it and asks explicit native consent before removing Internet/Restricted-zone alternate-stream marks. It rejects missing, changed, traversing, linked or junction-backed runtime paths and rechecks after the dialog. It never recursively unblocks the application directory, changes global .NET trust, disables antivirus, or unblocks project sources. Checksums establish consistency with the already-launched executable, not publisher authentication. The unsigned-release trust decision remains with the user. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 5f3a2c4..33ed9ba 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -86,3 +86,7 @@ Review diagnostics before sharing them. Never attach `credentials.dpapi`, Codex ## Angles are too cautious or ignore my voice Use v0.6.0 or newer. The authorial-intent changes introduced in v0.4 remove forced counterbalancing and supplies voice context to the early editorial stages. In an existing project, choose **current mode · Change -> Polemic**, then edit **Core thesis** and **Rhetorical force**. In Angles, choose **Refresh angles with my voice**; old saved angles do not rewrite themselves when a slider changes. The existing graph is reused. Evidence detail is a separate control and never sets how moderate your position must be. Ordinary Nonfiction can also **Develop and defend my position**. + +## Python.Runtime.Loader.Initialize failure + +This identifies Python.NET assembly loading, not by itself a missing WebView2 installation. A Windows Internet-download mark on `Python.Runtime.dll` reproduces this exact failure. Use the scoped v0.7.1 runtime-permission dialog or follow [downloaded-runtime permission](WINDOWS.md#downloaded-runtime-permission) for an older ZIP. Mismatched files, missing .NET dependencies and restrictive machine policy require inspecting the actual log rather than repeatedly reinstalling WebView2. diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index 660dc1b..6639134 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -54,3 +54,11 @@ For model, import and manuscript problems, continue with [Troubleshooting](TROUB ## External Graphify The current Windows package includes the public Graphify runtime and its dependencies. No separate Graphify installation is required. Clear an old custom executable path to select the included runtime; custom paths are never silently overridden. [Detailed setup and distribution](UPDATE-0.3.1.md). + +## Downloaded runtime permission + +In v0.7.1, GraphPaper checks its packaged window DLLs before loading Python.NET. Windows may mark files extracted from a downloaded ZIP as Internet files, preventing .NET from loading them. When needed, a native **GraphPaper runtime permission** dialog lists the affected, checksum-matched DLLs. Choose **Yes** only when you trust the release you downloaded. **No** cancels startup without changing files or opening the project database. This repair changes only the download marks on the listed bundled runtime files, not their bytes, your projects, unrelated files, antivirus settings, or global Windows security policy. Per-build hashes prevent mixed/corrupt runtimes being unblocked; they are not a substitute for trusting the release's publisher. + +For an older release reporting **Failed to resolve Python.Runtime.Loader.Initialize**, close the app. In Explorer, right-click the original release ZIP, choose **Properties → Unblock → Apply**, then extract the complete ZIP into a fresh folder. Keep `_internal` beside `GraphPaper.exe`; your project data directory stays separate. This is a repair for a downloaded-file block, not every possible .NET initialization failure. If there is no Unblock checkbox or the failure persists, retain `%LOCALAPPDATA%\GraphPaper\desktop.log` (or the log in your configured data directory) for diagnosis. Do not disable antivirus or change system-wide .NET trust settings. + +The release test now extracts the actual ZIP, simulates Internet-zone marks on DLLs, exercises both native consent choices, and checks the full native workflow. It does not claim to simulate SmartScreen or every managed Windows policy. diff --git a/docs/validation/v0.7.1/download-mark-reproduction.json b/docs/validation/v0.7.1/download-mark-reproduction.json new file mode 100644 index 0000000..8fe93d2 --- /dev/null +++ b/docs/validation/v0.7.1/download-mark-reproduction.json @@ -0,0 +1,23 @@ +{ + "versions": { + "pywebview": "6.2.1", + "pythonnet": "3.2.1", + "clr_loader": "0.3.1" + }, + "plain": { + "returncode": 0, + "stdout": "Python.NET initialized\n", + "stderr": "" + }, + "internet_marked": { + "returncode": 1, + "stdout": "", + "stderr": "Traceback (most recent call last):\n File \"\", line 1, in \n File \"C:\\hostedtoolcache\\windows\\Python\\3.12.10\\x64\\Lib\\site-packages\\clr.py\", line 6, in \n load()\n File \"C:\\hostedtoolcache\\windows\\Python\\3.12.10\\x64\\Lib\\site-packages\\pythonnet\\__init__.py\", line 143, in load\n func = assembly.get_function(\"Python.Runtime.Loader.Initialize\")\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"C:\\hostedtoolcache\\windows\\Python\\3.12.10\\x64\\Lib\\site-packages\\clr_loader\\types.py\", line 95, in get_function\n return ClrFunction(self._runtime, self._path, name, func)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"C:\\hostedtoolcache\\windows\\Python\\3.12.10\\x64\\Lib\\site-packages\\clr_loader\\types.py\", line 59, in __init__\n self._callable = runtime._get_callable(assembly, typename, func_name)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"C:\\hostedtoolcache\\windows\\Python\\3.12.10\\x64\\Lib\\site-packages\\clr_loader\\netfx.py\", line 47, in _get_callable\n raise RuntimeError(\nRuntimeError: Failed to resolve Python.Runtime.Loader.Initialize from C:\\hostedtoolcache\\windows\\Python\\3.12.10\\x64\\Lib\\site-packages\\pythonnet\\runtime\\Python.Runtime.dll\n" + }, + "unblocked": { + "returncode": 0, + "stdout": "Python.NET initialized\n", + "stderr": "" + }, + "reproduced": true +} \ No newline at end of file diff --git a/docs/wiki-pages.json b/docs/wiki-pages.json index 9f45df5..812fa93 100644 --- a/docs/wiki-pages.json +++ b/docs/wiki-pages.json @@ -1,6 +1,6 @@ { "repository": "AronAxe/GraphPaper", - "docs_version": "0.7.0", + "docs_version": "0.7.1", "pages": [ {"title":"Home","source":"docs/README.md","section":"Start here"}, {"title":"Visual-Tour","source":"docs/VISUAL-TOUR.md","section":"Start here"}, diff --git a/graphpaper/__init__.py b/graphpaper/__init__.py index 49e0fc1..a5f830a 100644 --- a/graphpaper/__init__.py +++ b/graphpaper/__init__.py @@ -1 +1 @@ -__version__ = "0.7.0" +__version__ = "0.7.1" diff --git a/graphpaper/_runtime_hashes.py b/graphpaper/_runtime_hashes.py new file mode 100644 index 0000000..421d4e9 --- /dev/null +++ b/graphpaper/_runtime_hashes.py @@ -0,0 +1,2 @@ +"""Filled by scripts/runtime_manifest.py immediately before the Windows build.""" +FILES = {} diff --git a/graphpaper/desktop.py b/graphpaper/desktop.py index 0d03b5d..dc8e9be 100644 --- a/graphpaper/desktop.py +++ b/graphpaper/desktop.py @@ -129,6 +129,9 @@ def run(browser: bool = False): root = data_directory() root.mkdir(parents=True, exist_ok=True) logging.basicConfig(filename=str(root / "desktop.log"), level=logging.WARNING) + if not browser: + from .windows_runtime import prepare + prepare() app = create_app(root) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Keep the ephemeral listener open until handed to uvicorn (no port-selection race). @@ -225,15 +228,18 @@ def main(): if args.self_test: self_test(args.self_test) return + from .windows_runtime import RepairDeclined, startup_advice try: run(args.browser) + except RepairDeclined: + return except Exception as e: logging.exception("GraphPaper could not start") try: import tkinter as tk from tkinter import messagebox root = tk.Tk(); root.withdraw() - messagebox.showerror("GraphPaper could not open", f"{e}\n\nOn Windows, install Microsoft Edge WebView2 Runtime.\nDetails are in GraphPaper's local desktop.log.") + messagebox.showerror("GraphPaper could not open", f"{e}\n\n{startup_advice(e)}\nDetails: {data_directory() / 'desktop.log'}") root.destroy() except Exception: print(f"GraphPaper could not start: {e}", file=sys.stderr) diff --git a/graphpaper/windows_runtime.py b/graphpaper/windows_runtime.py new file mode 100644 index 0000000..9603f78 --- /dev/null +++ b/graphpaper/windows_runtime.py @@ -0,0 +1,132 @@ +"""Repair only verified, bundled window DLL download marks, with explicit consent. + +Never loads remote assemblies, changes machine security policy, or recursively +unblocks a user's folder. The per-build hashes live inside the frozen executable. +""" +from __future__ import annotations +import hashlib +import logging +import re +import stat +import sys +from pathlib import Path, PurePosixPath +from typing import Callable + +PREFIXES = ('pythonnet/runtime/', 'webview/lib/', 'clr_loader/ffi/dlls/') +PRIMARY = 'pythonnet/runtime/Python.Runtime.dll' + + +class RuntimePackageError(RuntimeError): + pass + + +class RepairDeclined(Exception): + """The user chose not to trust the downloaded packaged DLLs. Exit quietly.""" + + +def _path(root: Path, relative: str) -> Path: + parts = PurePosixPath(relative).parts + if (not relative.startswith(PREFIXES) or not relative.lower().endswith('.dll') + or '\\' in relative or ':' in relative or '..' in parts + or PurePosixPath(relative).is_absolute() or not parts): + raise RuntimePackageError('Invalid bundled runtime path.') + path = root + for part in parts: + path = path / part + try: + info = path.lstat() + except FileNotFoundError as exc: + raise RuntimePackageError('The application bundle is incomplete: '+relative+'. Extract the complete ZIP into a fresh folder.') from exc + if stat.S_ISLNK(info.st_mode) or getattr(info, 'st_file_attributes', 0) & 0x400: + raise RuntimePackageError('A bundled runtime path is a link or junction: '+relative) + if not path.is_file() or root.resolve() not in path.resolve().parents: + raise RuntimePackageError('Invalid bundled runtime file: '+relative) + return path + + +def file_hash(path: Path) -> str: + with path.open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + + +def verify_bundle(root: Path, hashes: dict[str, str]) -> dict[str, Path]: + if not hashes or PRIMARY not in hashes or len(hashes) > 100: + raise RuntimePackageError('The packaged runtime manifest is missing. Extract a complete official GraphPaper release.') + paths = {} + for relative, expected in hashes.items(): + if not isinstance(expected, str) or not re.fullmatch('[a-f0-9]{64}', expected): + raise RuntimePackageError('Invalid runtime checksum.') + path = _path(root, relative) + if file_hash(path) != expected: + raise RuntimePackageError('The runtime file does not match this executable: '+relative+'. Do not mix files from different releases; extract the ZIP into a fresh folder.') + paths[relative] = path + return paths + + +def _zone(path: Path) -> Path: + return Path(str(path)+':Zone.Identifier') + + +def repair_bundle(root: Path, hashes: dict[str, str], confirm: Callable[[list[str]], bool]) -> list[str]: + """Return names repaired; default behavior cannot authorize itself.""" + paths = verify_bundle(root, hashes) + marked = [] + for name, path in paths.items(): + try: + with _zone(path).open('rb') as stream: + value = stream.read(8192) + except FileNotFoundError: + continue + except OSError as exc: + raise RuntimePackageError('Could not inspect the download mark on '+name+'. Check folder permissions.') from exc + if re.search(rb'(?mi)^\s*ZoneId\s*=\s*[34]\s*$', value): + marked.append(name) + if not marked: + return [] + if not confirm(marked): + raise RepairDeclined() + # The dialog can remain open a long time: verify all bytes and paths again + # before making any changes. Do not trust an altered manifest on disk. + paths = verify_bundle(root, hashes) + for name in marked: + try: + _zone(paths[name]).unlink(missing_ok=True) + except OSError as exc: + raise RuntimePackageError('Could not unblock '+name+'. Close GraphPaper, open the downloaded ZIP Properties, choose Unblock, then extract into a fresh writable folder.') from exc + logging.warning('Author approved removal of Internet-zone marks from bundled runtime files: %s', ', '.join(marked)) + return marked + + +def _confirm(names: list[str]) -> bool: + import ctypes + from ctypes import wintypes + box = ctypes.WinDLL('user32', use_last_error=True).MessageBoxW + box.argtypes = [wintypes.HWND, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.UINT] + box.restype = ctypes.c_int + message = ("Windows marked GraphPaper's window components as Internet downloads and may block them from loading.\n\n" + "These files match the checksums embedded in this executable. Allow GraphPaper to remove their download marks?\n\n" + + '\n'.join(names) + + "\n\nChoose Yes only if you trust the GraphPaper release you downloaded. No documents, unrelated files or Windows-wide security settings will change. No cancels startup.") + return box(None, message, 'GraphPaper runtime permission', 0x4 | 0x20 | 0x100) == 6 + + +def prepare() -> list[str]: + if sys.platform != 'win32' or not getattr(sys, 'frozen', False): + return [] + from ._runtime_hashes import FILES + return repair_bundle(Path(sys._MEIPASS), FILES, _confirm) + + +def startup_advice(error: Exception) -> str: + text = str(error).lower() + if isinstance(error, RuntimePackageError): + return 'The packaged runtime could not be prepared. Your project data has not been replaced.' + if 'python.runtime' in text or 'clr_loader' in text: + return ('The Python/.NET bridge could not load. A downloaded-file block or mismatched runtime files can cause this. ' + 'For an older build, right-click the original release ZIP, choose Properties > Unblock, and extract the complete ZIP into a fresh folder. ' + 'If no Unblock checkbox exists or this persists, keep the local desktop.log for diagnosis; this message alone does not establish the cause.') + if 'webview2' in text or 'edgechromium' in text: + return 'The window renderer could not initialize. Check that Microsoft Edge WebView2 Runtime is installed; see desktop.log for the actual renderer error.' + if '.net runtime' in text or 'netfx' in text: + return 'The Windows .NET runtime could not initialize. Check the supported .NET Framework installation and desktop.log.' + return 'See the actual error and GraphPaper\'s local desktop.log. A window-runtime reinstall is not a general remedy for this error.' diff --git a/publish-manifest.json b/publish-manifest.json index 0d53da9..8d0da58 100644 --- a/publish-manifest.json +++ b/publish-manifest.json @@ -3,23 +3,25 @@ "files": { ".gitattributes": "acdb1038616f95b335ceb851c161a6139bf23561b2a74894a9ccb33ded5e71f4", ".github/topics.json": "341aa983b1829624dec7ab7cd2e83eed08d45bc78305033c1e860631c1227e41", + ".github/workflows/apply-runtime-fix.yml": "c4e52e7e6d8e6e69e853716515e2d7a89abb3cb0ea4267ff8e5579c840291ddf", ".github/workflows/docs.yml": "839af4553785c5a2b04a2647791f4056ce7bdb97d20cb5e56bddd65e0d429c68", ".github/workflows/graph-studio-release.yml": "f4e3ff44a859e65eb0ed9b4b5094a3011e1d00594c3ea94181ec88f66afa04aa", ".github/workflows/import-build.yml": "c56a990b97242f154907f76059f5b859448ca8de7b582928d63bb3717ac0d60a", ".github/workflows/native-diagnostics.yml": "818584858c3f0dce8ce17ff0ccff9b5984fab6c6d2a1481baa280f841c8cbf53", ".github/workflows/native-probe.yml": "b7e6f3ea3f8fef9e3725ac2a242b7c4be2545bc4491be5ce2e68f723c302bc56", ".github/workflows/quality.yml": "d7da2adb2caf8fb97c58f64ce2dc1025c2958c2c6672c0b7accb9e4556131654", + ".github/workflows/runtime-diagnostics.yml": "1fcd8dd071a78c8a23de790447266f9c51feb5cd6ce27dbfc3587f358227d0c6", ".github/workflows/voice-manifest.yml": "19647138870a09bbe7835df34ddab4017a91da9c1937444a32f3a35b9ef1fa88", ".github/workflows/windows.yml": "65512954b719620063384ce93950b89c9b0aaac2ad6c7c620d505f8787b0444a", ".gitignore": "229a84e2b07c91b31c30f06df236ea17fe211eb75b217adc2eecb3f9d377f9bf", - "CHANGELOG.md": "e288d09d51cac8ee952e63958b8ca8e23e2d77f1b5d51def57b2ed4e7f3dbd1a", + "CHANGELOG.md": "6edf4b4b5472dd0c2176cccf95f9b184f7fe017c79f3f7ae129888b548922504", "CONTRIBUTING.md": "170e7cd71886a6cd3aa82c3ab5a478cde152faa4f243900e047bca565e658001", "GraphPaper.pyw": "8fd19e9ba8caa25b11b0b458e00aa3ea4067ca5d90161f7ba2781f1d514fa094", "LICENSE": "5849844cbc9e9e199cf576224279d63d5fa56ca913f7a13285e4aa4fcb99bf80", "Open GraphPaper.vbs": "6ddf70eba29c281f9373c11b6aa7f69862c8ea7b03050b974c98d817ca11974e", "Publish GraphPaper.pyw": "a55984668dd0040d9bb5fc8c4c18ea5a201eede7e8a51ab79f8f0f095162ec8b", "Publish GraphPaper.vbs": "f9c595bcd13da5df60dcc2cfd906665243628f6e6c10a8b6a03ad1392a9698ad", - "README.md": "a3525923211fd8d52b743e60229784933b6492d108d3e114f3c80f54d548ec3a", + "README.md": "21b2c50a8e105698e8bede7b0433b7143514ea7dc55e711cd17753c2dc1bbf9d", "SECURITY.md": "e70e5b87ea06cdc075c3111a1eb0bb6ac5f5b6f8b55f5e735ad69d84ae63cda6", "docs/ARCHITECTURE.md": "86a28806513cf8b533d6a4767322c7dd339a6a2ed112081ea7ce5fcdf4ffd89d", "docs/CONNECTIONS.md": "28795d30a3ae8f64b5aac1c7eaf6adeb945c933cccb22bfb01e58f91f490909e", @@ -36,7 +38,7 @@ "docs/PROJECTS-AND-SOURCES.md": "0cc5d1224ef5ddeaaf020865648c7daed0bb7f7323906d094ade2968d010d453", "docs/PUBLISHING.md": "324b83d5138b22bb29ca1737cbc0563595cc14f03177d478c478bde9109f975f", "docs/QUALITY.md": "924816ff2c38e1401f1e330925311343e42fdba5c91acd65421b8c51a79ed45e", - "docs/README.md": "40152366111bd89cb5ccb17bb0d99b7272ae330cb785c6861ade8e433a912cef", + "docs/README.md": "c247ed9ad2f70e9e485a478cf0b1cb037bd212d2c3ffa5224b1bacd565e4addd", "docs/RELEASE-0.2.1.md": "42642432ff5c6017630d33a64726c5dca37abbbb36a476a901a59ec8887aa870", "docs/RELEASE-0.2.md": "c03a41903088590845d0da0707a562b9e6cbf4c5e1ff57d8794763f349093e44", "docs/RELEASE-0.3.0.md": "e32f30a3a9716c310be21dcf540cf9060384b0c7fdf69c8fc1c45d36c105cf29", @@ -45,17 +47,18 @@ "docs/RELEASE-0.5.0.md": "960b36e74d2044bdc05c8b26d54113ea6580c259103d6ef6017809822032b0bf", "docs/RELEASE-0.6.0.md": "02e6d3ef374c2b1309e416b2c11e93260bd83f00fa8c577471a7a9c7aab95900", "docs/RELEASE-0.7.0.md": "5c3fd972bbb8be108563421ce4b1e9770eeb3ffd1da6a147ad87d225452543a1", + "docs/RELEASE-0.7.1.md": "bb1315fc85b2c34f0055a3e96a5fb40cd927e8142b004a604ad93b9d566b4d9f", "docs/SCIENCE.md": "1190f85ee558febe0d19da47f8dde73d02916a54890f035fda4b9aa03108ed90", - "docs/SECURITY.md": "0f818eb0cf4e8f94ffaaeb6da3cc2c2bc474674b673d185c61b86b2eb5fd3864", + "docs/SECURITY.md": "81cee74721d183d124911c9905d311a8e39cdaffe81110e0322d00c29f508b23", "docs/SYSTEM-ONE.md": "1d5f9277b06109d9ab3abe82c5566356e4dd8da18e1b8594ae819c1c0b05b5b5", - "docs/TROUBLESHOOTING.md": "90a67baa584fce05c84e5d11f1d50ab6a315febaeaa56fd0156c39974a3ccf4e", + "docs/TROUBLESHOOTING.md": "1bc4b3471f4206fa4ab45868c84afacfbbfbf9381b7299be3dd592ba07fcfec7", "docs/UPDATE-0.2.md": "cd71d4bb37bfdbb859fa4ea558a91e0cb61f900f0a4390d87f513abb8b8d815d", "docs/UPDATE-0.3.1.md": "c1232dc6067211467ec713703966472bf75e22a114d62d0f5e0a40acdabff84f", "docs/UPDATE-0.3.md": "99076ec9ea12986d79b93ec25a729c90403d3432d8e51598db5903b862fba459", "docs/VISUAL-TOUR.md": "4d7d5161413bb8a5b03fd05aa23cf17653e3234c5265c36099d633e978163852", "docs/VOICE-AND-PROSE.md": "c4974561cba55bff002db17d0ca28a98bffd0c840b418932ee2cc4c66b22e18e", "docs/VOICE-GRAPHS.md": "145ee38601fb73415a008443c474e8173a1d3d66a20064533b175a8be0264d8e", - "docs/WINDOWS.md": "5dbc080f0dc1e45ee8c39cbe99d3525efecd60c7bdb9b4fc05757a4a07f0587f", + "docs/WINDOWS.md": "7c34664a7c26f3560ee34b20fe7d061f03451f5500cbf9c905eded44dd842bf3", "docs/images/angles-v0.6.webp": "e0ac8d73f231cc9baa3733eb9ac192b603357408cc6a5271ee8cd45b6f259755", "docs/images/angles.webp": "1f9a0b5b46c9ae7e84ed1f96ea909d8ca6ba3f7daad2d86f424332c0aac1fb4e", "docs/images/apa-preview-v0.3.webp": "fc2ac3803d88c782e2a02cdfa345ce095e29a184a0ce7d0d6cdc3e7e024aabb0", @@ -154,17 +157,19 @@ "docs/validation/v0.4.0/package.json": "95434f0949b5ec5018287f8fb2b73cea28e8337c1cb4bbb2c93f0997c25114ad", "docs/validation/v0.4.0/summary.json": "3e50403b4db2359985c803f0b275aee1fb69ca7fb7a53a4cbbb2d5d6eaa52bdc", "docs/validation/v0.4.0/windows-unit.xml": "aa1a17c42d241651f9c0b91599d6683ab038644b867127f2d36b19eebdd6eb51", - "docs/wiki-pages.json": "5e871d3fa310bfbbe794831d89015d8a443f996a217b148334233ae4189541c0", + "docs/validation/v0.7.1/download-mark-reproduction.json": "468232e6bce3496069bafa79a749c718c56bc21a2c3b71a1106cce3ae8a15e18", + "docs/wiki-pages.json": "18841d13933deab5131971e5820900bbdd5adb9f54f95d8a5b520d1b65fb3ced", "examples/README.md": "0b7b83630202501a3ba6ad070708419aa23e32c8060ef9447491c6434043bbf6", "examples/fiction.json": "e3f90c0eed71102640c8a65ae86510e0258e9c90c3cc18730ed05329f632e88d", "examples/nonfiction.json": "af9196cf15dbe1d431f02c7d8ec11a720f25e1c3e30a756c1f9711b4c65a08cc", - "graphpaper/__init__.py": "45a00d19b46ee5efaf7a1c17235f907b68203cf7ecd5341a663d3bd8975b2e4e", + "graphpaper/__init__.py": "d8a2590d230e1bb292f3603160ead9e198a16315f4c237d48c3b08661dcbd36e", + "graphpaper/_runtime_hashes.py": "5143b16bda72a3ad7ea81a9cb5922995ee578f0f2857f710098bbc16ed6c1cb8", "graphpaper/apa.py": "e8c5b844c0f02f14c5a73d6128e4d174d4ad655e380b4d675215beb125cd717f", "graphpaper/argument_draft.py": "0b634ab7000f1d861a1100482dc6d7d3ddc4564a3644ed1b051ef173b53d3427", "graphpaper/author_web.py": "16ad1218ce0bf76bdd067e731176357586a28dfb55d297a393a3e6a596e126ff", "graphpaper/codex.py": "29361635a46669b5c0ce7d06b903902f05a007424c0061a0df983bf6c81af95f", "graphpaper/demo.py": "5fcee68b786da702cd4365dca17e3f5df415fa26c7c6928568d64d8b7d693006", - "graphpaper/desktop.py": "1038d7bee11976151522cb46ca297717f1150cf7505c9a2e0c54002b9386629d", + "graphpaper/desktop.py": "88b28a6510876ff28f7b2c43dc15e7059b61d2de1ad8848d724bf5c22112db9d", "graphpaper/editorial.py": "ebbd847e987da10fe02b1a7d3187ef2f0628a9fc5b55f2269d4baece77f8a755", "graphpaper/editorial_routes.py": "a62e6cb5854f49fdc2fda51337d3f639bb8bfcb945c9e17c377d3d010d98bd7d", "graphpaper/export.py": "fd76bec7858c53700dfe068035c187277ed85fa7e90a46cb1f1c47b57eb1e744", @@ -206,15 +211,17 @@ "graphpaper/voice.py": "74be72d1e6edb70a6b541547836db4a7c4af575314c92a253395f9ede26b7976", "graphpaper/voice_library.py": "2db504911796b564489494b95628fde606b834f1ab890443eea0c2f9db7e670e", "graphpaper/voice_routes.py": "725a858590e93b9f51ecb95378713ae8e8e88dad99fb1c903889f02a272e4532", - "pyproject.toml": "f6c271eca90bfb756c8f1380e1f2031a27617589863c13fc7382a9d265bb0c8b", + "graphpaper/windows_runtime.py": "976fa06f1c85f98db0e8676e9710e21595cdbbfe128b27e245872c24054198ec", + "pyproject.toml": "aaa97251b0748635ede0f7df7b36f36d2bb93c0f490710ee74e6f470b813df52", "requirements-desktop.txt": "c9df0bdb57d7e9586ae7c927b5dca32f6e20010e71fc71cc4a5656387a129892", "requirements-dev.txt": "40b197f9671e3c6db4d47ca73d9679eabafe662a56d4824c6f9e64255e47a5ce", "requirements-graphify.txt": "9eea3e6ea7fa3ceb1247518901d41d55a0813dc5133475172541c68c122454f1", "requirements.txt": "4e73f195c8a6d32b841b671200bf0dcaa91e5a042c585e935e3132ee5e4a02a3", - "scripts/build_release.ps1": "cd51fd6d1f3001abe66d4cc5d782c8bc3dd00f54cfa7e42f88a935787bf0b3c9", + "scripts/build_release.ps1": "c0be03d1862bc4623e5bc0ce2d7562f0c448f2d5dea1f87d1cd03bdb85fa0323", "scripts/build_windows.ps1": "44e0008ff8221097ead7355aa184a60e824d2fcbf0c77e54c74ddfc8f3a54858", "scripts/bundle_codex.py": "651d08e26949dbda47bca14dbc368d97b82e11a28759792cb6096adcea174efa", "scripts/check_codex.py": "2fff4a6cc80188d54914086ab3f3d7d3cf1ec02001541bad6bc8a426fc1bbc81", + "scripts/check_downloaded_release.py": "70b4091f65839262d343c28cd03396ea577dfd89ff8ba9c403db3fbbedeb9ba5", "scripts/check_graphify.py": "355f5f04011d4bb12778de9d2c18a654a8f3d4b1bcdbb353cc1e028c93950968", "scripts/check_package.py": "ef863315ea3f21a8f94b412276849df73d1b4fb0aaa30d7890a62a426a6cc4b7", "scripts/check_polemic_live.py": "7dede3422b5daddbf52ecce311a45ddd10307a7e19ad7eef5dc42d22dc839960", @@ -224,13 +231,14 @@ "scripts/graph_studio_ui_checks.py": "7f3e05bd9649666854521eabfccb650e6c3d15578633ef36a027877828c6d267", "scripts/graphify_ui_checks.py": "6dc1e45478a10fa8e3d17190864b2145cd973a5a6e23c7500f0d99ddfb824dc6", "scripts/hooks/hook-graphify.py": "5659b57f3b3e11f9327b2da6e00c4bd82a523f7f978e3b9506811c2caf007ff3", - "scripts/native_smoke.py": "ca8a54a283887174aec39e3e060232542a9cb836d6a8225a307a1fa992538097", + "scripts/native_smoke.py": "5e5aba0f9c736b654acc4d4cd99fe61eb73657638ab0bc8b550e602bd742c980", "scripts/package_windows.py": "8b56c4235ca2bbac39657905fa4633d78273bc4df6c9e67558a7e893176af8b3", "scripts/prepare_assets.py": "179b65a0662d329fadecd9a41c134a249ed1f3cc3123a814095ee231636d819d", "scripts/prepare_ci_webview.ps1": "2572906263473c7ea94bd9517e76b2e1470ffec1719b6244088ac2a9346e6195", "scripts/prepare_graphify.py": "c81d4146c3b4a137b93bb61d16308c483a3e3a453501426569242d227bb58e03", "scripts/probe_ci_cdp.py": "91b76aa4aa813792088b435d35144b32f94dd49e8c7248a90b56c3e247dc81c9", "scripts/publish_gui.py": "46bf7dff734e16b31c322e45bbf80abcfa29d7fbbc93f4c8162a119ff9c79ef4", + "scripts/runtime_manifest.py": "f85c339d5f1fab0cb15c7176cf611bc0c2bb50e556e209514e290b0c6ce7605d", "scripts/system_one_ui_checks.py": "8123b985d8a104ff48f10283438f156b644ef7cce9d84b49f2e95251212c88ac", "scripts/ui_graph_studio_smoke.py": "9c398f0e14a899768e89b9212c0fa07b02e77e533c496b31b2e8d0735dde19f8", "scripts/ui_graphify_smoke.py": "0f29a01d972ad63006aba345952bb9e092575c452eb9475c505c21d9267a3de3", @@ -279,6 +287,7 @@ "tests/test_voice_integration.py": "63c9e4d4dadba187d36169593868af44a4df94896ae1dc06b8b4f451ac0f8eab", "tests/test_voice_library.py": "12071c049d7ccca7d10d531879ed67c19eee97178c0a5c3bc34559aed77417e7", "tests/test_windowless.py": "421620004c87cf24dec771966602b0666e75ecca67fec05071cf40e4b6887f44", + "tests/test_windows_runtime.py": "db61429d4e86741a9d0b636d8ad64aae0f57c35b2d4efb097182fd52a5fc20d7", "ui/app.js": "4f5681c432b8fa5923fa68d57b9ace55cd493ee43be5971316a632f73ec2917d", "ui/assets/CREDITS.md": "e9672e1c683f7db3e5265c52ea98fda9e8eec260e0a251ffa3065e85ebcffd77", "ui/assets/graph-paper.json": "0b37ff27703f8f6adfcd4d05037a594d526749d9641cb6cd226efe1dcc09f687", diff --git a/pyproject.toml b/pyproject.toml index 26b6045..44323c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphpaper-studio" -version = "0.7.0" +version = "0.7.1" description = "A local-first graph-guided writing studio for nonfiction and fiction" readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/build_release.ps1 b/scripts/build_release.ps1 index 6c23177..54e52ca 100644 --- a/scripts/build_release.ps1 +++ b/scripts/build_release.ps1 @@ -8,11 +8,15 @@ python scripts/check_codex.py if ($LASTEXITCODE -ne 0) { throw 'Codex runtime protocol check failed.' } python scripts/prepare_graphify.py if ($LASTEXITCODE -ne 0) { throw 'Public Graphify runtime verification failed.' } -python -m PyInstaller --noconfirm --clean --windowed --onedir --name GraphPaper --paths . --additional-hooks-dir scripts/hooks --icon ui/graphpaper.ico --add-data 'ui;ui' --add-data 'vendor;vendor' --collect-all webview scripts/desktop_entry.py +python scripts/runtime_manifest.py +if ($LASTEXITCODE -ne 0) { throw 'Runtime manifest generation failed.' } +python -m PyInstaller --noconfirm --clean --windowed --onedir --name GraphPaper --paths . --additional-hooks-dir scripts/hooks --icon ui/graphpaper.ico --add-data 'ui;ui' --add-data 'vendor;vendor' --collect-all webview --collect-all pythonnet --collect-all clr_loader scripts/desktop_entry.py if ($LASTEXITCODE -ne 0) { throw 'Windows desktop build failed.' } +python scripts/runtime_manifest.py --verify dist/GraphPaper/_internal +if ($LASTEXITCODE -ne 0) { throw 'Frozen runtime checksum verification failed.' } Copy-Item README.md, LICENSE -Destination dist/GraphPaper/ Copy-Item ui/assets/CREDITS.md -Destination dist/GraphPaper/ASSET-CREDITS.md -Copy-Item docs/RELEASE-0.7.0.md -Destination dist/GraphPaper/WHATS-NEW.md +Copy-Item docs/RELEASE-0.7.1.md -Destination dist/GraphPaper/WHATS-NEW.md python scripts/check_package.py dist/GraphPaper if ($LASTEXITCODE -ne 0) { throw 'Compiled application self-test failed.' } python scripts/check_graphify.py --executable dist/GraphPaper/GraphPaper.exe --out test-results/graphify-package.json diff --git a/scripts/check_downloaded_release.py b/scripts/check_downloaded_release.py new file mode 100644 index 0000000..d75faf4 --- /dev/null +++ b/scripts/check_downloaded_release.py @@ -0,0 +1,41 @@ +"""Test a fresh extraction of the actual ZIP with simulated Windows Internet-zone marks.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import zipfile + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('archive',type=Path) + parser.add_argument('--out',type=Path,default=Path('test-results/downloaded-package')) + args=parser.parse_args() + if sys.platform!='win32' or os.environ.get('GITHUB_ACTIONS')!='true' or os.environ.get('RUNNER_ENVIRONMENT')!='github-hosted': + raise SystemExit('Requires an isolated GitHub-hosted Windows runner.') + archive=args.archive.resolve();out=args.out.resolve();out.mkdir(parents=True,exist_ok=True) + with archive.open('rb') as stream:digest=hashlib.file_digest(stream,'sha256').hexdigest() + with tempfile.TemporaryDirectory(prefix='GraphPaper-downloaded-') as temporary: + target=Path(temporary) + with zipfile.ZipFile(archive) as bundle: + for entry in bundle.infolist(): + if not (target/entry.filename).resolve().is_relative_to(target):raise ValueError('Unsafe ZIP entry') + bundle.extractall(target) + executable=target/'GraphPaper/GraphPaper.exe' + assert executable.is_file() + subprocess.run([sys.executable,str(Path(__file__).with_name('native_smoke.py')), + '--executable',str(executable),'--downloaded-runtime','--science','--graphify', + '--polemic','--voices','--graph-studio','--system-one','--out',str(out)],check=True) + report=json.loads((out/'report.json').read_text(encoding='utf-8')) + assert report['ok'] and report['downloaded_runtime']['native_consent'] + report['archive_sha256']=digest + report['scope']='Actual frozen Windows app from the release ZIP. Internet-zone tags simulated on bundled DLLs; tests do not simulate SmartScreen, all antivirus products, or every Windows policy.' + (out/'report.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8') + print('Downloaded-runtime and complete native workflows passed for archive',digest) + + +if __name__=='__main__':main() diff --git a/scripts/native_smoke.py b/scripts/native_smoke.py index 67caabf..ce520d2 100644 --- a/scripts/native_smoke.py +++ b/scripts/native_smoke.py @@ -18,6 +18,7 @@ def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--executable',type=Path) + parser.add_argument('--downloaded-runtime',action='store_true',help='Hosted-only: simulate Internet-zone tags and exercise native repair consent') parser.add_argument('--trace-source',action='store_true',help='Capture Python thread stacks for this isolated source test only') parser.add_argument('--science',action='store_true',help='Exercise the Science workspace in the actual native shell') parser.add_argument('--graphify',action='store_true',help='Verify the actual external Graphify action using a synthetic loopback model') @@ -57,6 +58,18 @@ def check(name):report['checks'].append(name);print('PASS',name,flush=True) command=[str(Path(sys.base_prefix)/'python.exe'),'-c',trace_code] env['PYTHONPATH']=os.pathsep.join([str(ROOT),*sys.path]) report['debugger_configuration']='explicit isolated hosted test option' if hosted else 'WebView2 environment' + if args.downloaded_runtime: + if not hosted or not args.executable: + raise ValueError('Downloaded-runtime regression requires an isolated GitHub-hosted packaged build.') + internal=args.executable.resolve().parent/'_internal' + mark='[ZoneTransfer]\nZoneId=3\nHostUrl=https://github.com/AronAxe/GraphPaper/releases/\n' + marked_dlls=list(internal.rglob('*.dll')) + for dll in marked_dlls:Path(str(dll)+':Zone.Identifier').write_text(mark,encoding='utf-8') + sentinel=internal/'unrelated-regression-fixture.dll';sentinel.write_bytes(b'Not a loaded assembly') + Path(str(sentinel)+':Zone.Identifier').write_text(mark,encoding='utf-8') + document=data/'author-words.txt';document.write_text('Private project sentinel',encoding='utf-8') + Path(str(document)+':Zone.Identifier').write_text(mark,encoding='utf-8') + report['downloaded_runtime']={'simulated_dll_marks':len(marked_dlls),'native_consent':True} log=(out/'native.log').open('w',encoding='utf-8') proc=subprocess.Popen(command,cwd=ROOT,env=env,stdout=log,stderr=subprocess.STDOUT) def windows(parent=None): @@ -86,13 +99,33 @@ def wait_test(predicate,seconds=10): time.sleep(.1) raise AssertionError('Native test condition did not become true') try: + if args.downloaded_runtime: + def permission_dialog(): + return next((w for w in windows() if w['visible'] and w['title']=='GraphPaper runtime permission'),None) + dialog=wait_test(permission_dialog,30) + u.PostMessageW(dialog['hwnd'],0x111,7,0) # Real Win32 No button command. + proc.wait(timeout=10) + assert proc.returncode==0 + assert Path(str(internal/'pythonnet/runtime/Python.Runtime.dll')+':Zone.Identifier').exists() + assert not (data/'studio.sqlite3').exists() + check('Declining runtime permission exits without unblocking DLLs or opening project storage') + proc=subprocess.Popen(command,cwd=ROOT,env=env,stdout=log,stderr=subprocess.STDOUT) + dialog=wait_test(permission_dialog,30) + u.PostMessageW(dialog['hwnd'],0x111,6,0) # Real Win32 Yes button command. + check('Native downloaded-runtime repair requires and accepts explicit author consent') def cdp_ready(): if proc.poll() is not None:raise AssertionError(f'Native application exited during startup: {proc.returncode}') try: with urlopen(f'http://127.0.0.1:{port}/json',timeout=1) as r:targets=json.load(r) return any(t.get('url','').startswith('http://127.0.0.1:') for t in targets) except OSError:return False - wait_test(cdp_ready,25) + wait_test(cdp_ready,45) + if args.downloaded_runtime: + assert not Path(str(internal/'pythonnet/runtime/Python.Runtime.dll')+':Zone.Identifier').exists() + assert Path(str(sentinel)+':Zone.Identifier').exists() + assert Path(str(document)+':Zone.Identifier').exists() + assert document.read_text(encoding='utf-8')=='Private project sentinel' + check('Verified bundled runtime is unblocked while unrelated DLLs and author documents keep their download marks') native=wait_test(lambda:next((w for w in windows() if w['visible'] and w['title'].startswith('GraphPaper')),None)) hwnd=native['hwnd'];responsive(hwnd);check('Actual Windows/WebView2 window starts and responds to Windows messages') with sync_playwright() as pw: diff --git a/scripts/runtime_manifest.py b/scripts/runtime_manifest.py new file mode 100644 index 0000000..96e5d50 --- /dev/null +++ b/scripts/runtime_manifest.py @@ -0,0 +1,37 @@ +"""Embed per-build DLL checksums; verify actual frozen destinations before release.""" +from __future__ import annotations +import argparse +import importlib.metadata as metadata +from pathlib import Path +import pprint +import runpy +import sys + +ROOT=Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT)) +from graphpaper.windows_runtime import PREFIXES, PRIMARY, file_hash, verify_bundle + + +def generate(): + files={} + for name in ('pythonnet','pywebview','clr_loader'): + dist=metadata.distribution(name) + for item in dist.files or []: + relative=item.as_posix() + if relative.startswith(PREFIXES) and relative.lower().endswith('.dll'): + files[relative]=file_hash(Path(dist.locate_file(item))) + if PRIMARY not in files:raise RuntimeError('Python.NET runtime missing from build environment') + output=ROOT/'graphpaper/_runtime_hashes.py' + output.write_text('"""Generated DLL checksums embedded in this build; not publisher authentication."""\nFILES = '+pprint.pformat(dict(sorted(files.items())),sort_dicts=True)+'\n',encoding='utf-8') + print('Embedded',len(files),'window-runtime DLL hashes') + + +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--verify',type=Path) + args=parser.parse_args() + if args.verify: + hashes=runpy.run_path(str(ROOT/'graphpaper/_runtime_hashes.py'))['FILES'] + paths=verify_bundle(args.verify,hashes) + print('Verified frozen paths and bytes for',len(paths),'runtime DLLs') + else:generate() diff --git a/tests/test_windows_runtime.py b/tests/test_windows_runtime.py new file mode 100644 index 0000000..27fc615 --- /dev/null +++ b/tests/test_windows_runtime.py @@ -0,0 +1,85 @@ +"""Downloaded portable builds need scoped, consented runtime repair, not a global unblock.""" +import hashlib +import importlib.util +from pathlib import Path +import pytest + + +def runtime(): + assert importlib.util.find_spec('graphpaper.windows_runtime'), 'Downloaded-runtime handling is missing' + from graphpaper import windows_runtime + return windows_runtime + + +def bundle(tmp_path): + root=tmp_path/'_internal' + path=root/'pythonnet/runtime/Python.Runtime.dll' + path.parent.mkdir(parents=True) + path.write_bytes(b'packaged assembly fixture') + hashes={'pythonnet/runtime/Python.Runtime.dll':hashlib.sha256(path.read_bytes()).hexdigest()} + return root,path,hashes + + +def tag(path): + return Path(str(path)+':Zone.Identifier') + + +def test_marked_runtime_repair_preserves_bytes_and_unrelated_files(tmp_path): + mod=runtime();root,path,hashes=bundle(tmp_path) + tag(path).write_text('[ZoneTransfer]\nZoneId=3\n') + other=root/'unrelated.dll';other.write_bytes(b'not ours');tag(other).write_text('ZoneId=3') + document=tmp_path/'my project.txt';document.write_text('Author words');tag(document).write_text('ZoneId=3') + prompts=[] + result=mod.repair_bundle(root,hashes,lambda names:prompts.append(names) or True) + assert result==['pythonnet/runtime/Python.Runtime.dll'] + assert prompts==[['pythonnet/runtime/Python.Runtime.dll']] + assert not tag(path).exists() + assert hashlib.sha256(path.read_bytes()).hexdigest()==hashes[result[0]] + assert tag(other).exists() and tag(document).exists() and document.read_text()=='Author words' + + +def test_declined_repair_changes_nothing(tmp_path): + mod=runtime();root,path,hashes=bundle(tmp_path);tag(path).write_text('ZoneId=3') + with pytest.raises(mod.RepairDeclined):mod.repair_bundle(root,hashes,lambda names:False) + assert tag(path).exists() + + +def test_unmarked_runtime_does_not_prompt(tmp_path): + mod=runtime();root,path,hashes=bundle(tmp_path) + assert mod.repair_bundle(root,hashes,lambda _:pytest.fail('Unexpected consent prompt'))==[] + + +def test_corrupt_assembly_is_not_unblocked(tmp_path): + mod=runtime();root,path,hashes=bundle(tmp_path);tag(path).write_text('ZoneId=3');path.write_bytes(b'changed') + with pytest.raises(mod.RuntimePackageError,match='match'):mod.repair_bundle(root,hashes,lambda _:pytest.fail('Do not ask to trust mismatched files')) + assert tag(path).exists() + + +@pytest.mark.parametrize('relative',['../outside.dll','/root/evil.dll','webview/lib/../../evil.dll','webview/lib/x.dll:other','vendor/evil.dll']) +def test_manifest_cannot_escape_runtime_allowlist(tmp_path,relative): + mod=runtime();root,path,hashes=bundle(tmp_path);hashes[relative]='0'*64 + with pytest.raises(mod.RuntimePackageError):mod.repair_bundle(root,hashes,lambda _:True) + + +def test_symlink_not_followed(tmp_path): + mod=runtime();root,path,hashes=bundle(tmp_path);outside=tmp_path/'other.dll';outside.write_bytes(path.read_bytes());path.unlink() + try:path.symlink_to(outside) + except OSError:pytest.skip('Account cannot create symlinks') + tag(outside).write_text('ZoneId=3') + with pytest.raises(mod.RuntimePackageError):mod.repair_bundle(root,hashes,lambda _:True) + assert tag(outside).exists() + + +def test_changed_during_consent_is_rechecked(tmp_path): + mod=runtime();root,path,hashes=bundle(tmp_path);tag(path).write_text('ZoneId=3') + def consent(_):path.write_bytes(b'changed while dialog was open');return True + with pytest.raises(mod.RuntimePackageError,match='match'):mod.repair_bundle(root,hashes,consent) + assert tag(path).exists() + + +def test_error_message_does_not_misdiagnose_pythonnet_as_webview(): + mod=runtime() + text=mod.startup_advice(RuntimeError('Failed to resolve Python.Runtime.Loader.Initialize from D:\\Programs\\GraphPaper\\_internal\\pythonnet\\runtime\\Python.Runtime.dll')) + assert 'Unblock' in text and 'WebView2' not in text + assert 'WebView2' in mod.startup_advice(RuntimeError('WebView2 Runtime not found')) + assert 'WebView2' not in mod.startup_advice(RuntimeError('Database is locked')) From f65ab3ea1fc6bd3dc27c4f9ed5de2b6249d0110d Mon Sep 17 00:00:00 2001 From: Aron Bijl Date: Wed, 14 Oct 2026 13:30:13 +0200 Subject: [PATCH 4/9] Gate Windows 0.7.1 on downloaded-ZIP runtime consent and full native regression --- .github/workflows/apply-runtime-fix.yml | 54 ---------------------- .github/workflows/graph-studio-release.yml | 30 ++++++------ .github/workflows/runtime-diagnostics.yml | 46 ------------------ scripts/build_release.ps1 | 2 +- 4 files changed, 17 insertions(+), 115 deletions(-) delete mode 100644 .github/workflows/apply-runtime-fix.yml delete mode 100644 .github/workflows/runtime-diagnostics.yml diff --git a/.github/workflows/apply-runtime-fix.yml b/.github/workflows/apply-runtime-fix.yml deleted file mode 100644 index 45a7d4f..0000000 --- a/.github/workflows/apply-runtime-fix.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Apply verified Windows runtime repair -on: - push: - branches: [fix/windows-runtime-v0.7.1] - paths: ['.github/workflows/apply-runtime-fix.yml'] -permissions: - contents: write -jobs: - apply: - runs-on: ubuntu-latest - timeout-minutes: 8 - steps: - - uses: actions/checkout@v4 - with: - ref: fix/windows-runtime-v0.7.1 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Apply exact tested patch without changing workflow permissions - env: - GH_TOKEN: ${{ github.token }} - run: | - gh api repos/AronAxe/GraphPaper/git/blobs/076495d09cb43325e051fce5e120473353549af3 > /tmp/patch.json - python - <<'PY' - import base64,hashlib,json,lzma,subprocess - from pathlib import Path - raw=json.loads(Path('/tmp/patch.json').read_text()) - patch=lzma.decompress(base64.b64decode(raw['content'])) - assert hashlib.sha256(patch).hexdigest()=='ac2909c57f3ca5a956a2986ae6caeb27da78ee1703834b577935d405c7b596ff' - p=Path('/tmp/runtime.patch');p.write_bytes(patch) - subprocess.run(['git','apply','--check',str(p)],check=True) - subprocess.run(['git','apply',str(p)],check=True) - assert not subprocess.check_output(['git','diff','--name-only','--','.github/workflows']).strip() - PY - - run: python -m pip install -q -r requirements-dev.txt - - run: python -m pytest -q --junitxml=test-results/runtime-linux.xml - - run: python -m unittest discover -s docs/tests -p 'test_*.py' -v - - run: python scripts/docs_tool.py check - - name: Commit ordinary source only - run: | - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - python scripts/prepare_assets.py --manifest - git add publish-manifest.json - test -z "$(git diff --cached --name-only -- .github/workflows)" - git diff --cached --check - git commit -m 'Repair Internet-marked bundled DLLs with explicit consent and per-build checksums' - git push origin HEAD:fix/windows-runtime-v0.7.1 - - uses: actions/upload-artifact@v4 - if: always() - with: - name: runtime-repair-linux-tests - path: test-results/runtime-linux.xml diff --git a/.github/workflows/graph-studio-release.yml b/.github/workflows/graph-studio-release.yml index 5eb6e55..2d3763a 100644 --- a/.github/workflows/graph-studio-release.yml +++ b/.github/workflows/graph-studio-release.yml @@ -1,13 +1,13 @@ -name: GraphPaper 0.7 System One release +name: GraphPaper 0.7.1 downloaded-runtime release on: push: - branches: [main, feat/system-one-v0.7] + branches: [main, fix/windows-runtime-v0.7.1] paths: - 'graphpaper/**' - 'ui/**' - 'scripts/**' - 'tests/**' - - 'docs/RELEASE-0.7.0.md' + - 'docs/RELEASE-0.7.1.md' - '.github/workflows/graph-studio-release.yml' workflow_dispatch: permissions: @@ -97,11 +97,13 @@ jobs: run: ./scripts/build_release.ps1 - name: Exercise the compiled System One desk and existing workspaces run: python scripts/native_smoke.py --science --graphify --graph-studio --polemic --voices --system-one --executable dist/GraphPaper/GraphPaper.exe --out test-results/native-package + - name: Verify fresh ZIP extraction with Internet-zone DLL marks and native consent + run: python scripts/check_downloaded_release.py dist/GraphPaper-v0.7.1-Windows-x64.zip --out test-results/downloaded-package - uses: actions/upload-artifact@v4 with: - name: GraphPaper-0.7-tested-Windows + name: GraphPaper-0.7.1-tested-Windows path: | - dist/GraphPaper-v0.7.0-Windows-x64.zip + dist/GraphPaper-v0.7.1-Windows-x64.zip dist/SHA256SUMS.txt - uses: actions/upload-artifact@v4 if: always() @@ -121,7 +123,7 @@ jobs: ref: ${{ needs.source.outputs.revision }} - uses: actions/download-artifact@v4 with: - name: GraphPaper-0.7-tested-Windows + name: GraphPaper-0.7.1-tested-Windows path: release-package - name: Publish the same tested archive, never overwrite an existing release env: @@ -131,13 +133,13 @@ jobs: cd release-package sha256sum --check SHA256SUMS.txt cd .. - if gh release view v0.7.0 >/dev/null 2>&1; then echo 'Release already exists; refusing overwrite.'; exit 1; fi - cp docs/RELEASE-0.7.0.md release-package/notes.md + if gh release view v0.7.1 >/dev/null 2>&1; then echo 'Release already exists; refusing overwrite.'; exit 1; fi + cp docs/RELEASE-0.7.1.md release-package/notes.md revision=$(git rev-parse HEAD) - digest=$(sha256sum release-package/GraphPaper-v0.7.0-Windows-x64.zip | cut -d' ' -f1) - printf '\n\nCompiled Windows System One, graph editing and native save-on-close checks passed. Validation: https://github.com/%s/actions/runs/%s . Source: %s . Archive SHA-256: %s .\n' "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID" "$revision" "$digest" >> release-package/notes.md - gh release create v0.7.0 release-package/GraphPaper-v0.7.0-Windows-x64.zip release-package/SHA256SUMS.txt --target "$revision" --title 'GraphPaper 0.7.0 — System One decision studio' --notes-file release-package/notes.md --draft - release_id=$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100" --jq '.[]|select(.tag_name=="v0.7.0")|.id') - uploaded=$(gh api "repos/$GITHUB_REPOSITORY/releases/$release_id/assets" --jq '.[]|select(.name=="GraphPaper-v0.7.0-Windows-x64.zip")|.digest') + digest=$(sha256sum release-package/GraphPaper-v0.7.1-Windows-x64.zip | cut -d' ' -f1) + printf '\n\nCompiled Windows, fresh downloaded-ZIP runtime consent, System One and native save-on-close checks passed. Validation: https://github.com/%s/actions/runs/%s . Source: %s . Archive SHA-256: %s .\n' "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID" "$revision" "$digest" >> release-package/notes.md + gh release create v0.7.1 release-package/GraphPaper-v0.7.1-Windows-x64.zip release-package/SHA256SUMS.txt --target "$revision" --title 'GraphPaper 0.7.1 — Windows runtime startup fix' --notes-file release-package/notes.md --draft + release_id=$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100" --jq '.[]|select(.tag_name=="v0.7.1")|.id') + uploaded=$(gh api "repos/$GITHUB_REPOSITORY/releases/$release_id/assets" --jq '.[]|select(.name=="GraphPaper-v0.7.1-Windows-x64.zip")|.digest') test "$uploaded" = "sha256:$digest" - gh release edit v0.7.0 --draft=false --latest + gh release edit v0.7.1 --draft=false --latest diff --git a/.github/workflows/runtime-diagnostics.yml b/.github/workflows/runtime-diagnostics.yml deleted file mode 100644 index b29531f..0000000 --- a/.github/workflows/runtime-diagnostics.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Diagnose downloaded Windows runtime -on: - push: - branches: [fix/windows-runtime-v0.7.1] - paths: ['.github/workflows/runtime-diagnostics.yml'] -permissions: - contents: read -jobs: - diagnostics: - runs-on: windows-latest - timeout-minutes: 8 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - run: python -m pip install "pywebview>=6,<7" - - name: Compare the same Python.Runtime.dll before and after an Internet-zone tag - shell: python - run: | - import importlib.metadata as md, json, pathlib, subprocess, sys - root=pathlib.Path('test-results/runtime-diagnostics'); root.mkdir(parents=True,exist_ok=True) - dist=md.distribution('pythonnet') - dll=pathlib.Path(dist.locate_file('pythonnet/runtime/Python.Runtime.dll')) - assert dll.is_file() - tag=pathlib.Path(str(dll)+':Zone.Identifier') - def probe(): - p=subprocess.run([sys.executable,'-c','import clr; print("Python.NET initialized")'],capture_output=True,text=True,timeout=45) - return {'returncode':p.returncode,'stdout':p.stdout,'stderr':p.stderr} - report={'versions':{name:md.version(name) for name in ('pywebview','pythonnet','clr_loader')},'plain':probe()} - try: - tag.write_text('[ZoneTransfer]\nZoneId=3\nHostUrl=https://github.com/AronAxe/GraphPaper/releases/\n',encoding='utf-8') - report['internet_marked']=probe() - finally: - tag.unlink(missing_ok=True) - report['unblocked']=probe() - report['reproduced']=report['plain']['returncode']==0 and report['internet_marked']['returncode']!=0 and 'Python.Runtime.Loader.Initialize' in report['internet_marked']['stderr'] and report['unblocked']['returncode']==0 - (root/'report.json').write_text(json.dumps(report,indent=2),encoding='utf-8') - print(json.dumps(report,indent=2)) - subprocess.run(['git','archive','--format=zip','HEAD','-o',str(root/'source.zip')],check=True) - - uses: actions/upload-artifact@v4 - if: always() - with: - name: runtime-investigation - path: test-results/runtime-diagnostics/ - retention-days: 3 diff --git a/scripts/build_release.ps1 b/scripts/build_release.ps1 index 54e52ca..6dad2d4 100644 --- a/scripts/build_release.ps1 +++ b/scripts/build_release.ps1 @@ -10,7 +10,7 @@ python scripts/prepare_graphify.py if ($LASTEXITCODE -ne 0) { throw 'Public Graphify runtime verification failed.' } python scripts/runtime_manifest.py if ($LASTEXITCODE -ne 0) { throw 'Runtime manifest generation failed.' } -python -m PyInstaller --noconfirm --clean --windowed --onedir --name GraphPaper --paths . --additional-hooks-dir scripts/hooks --icon ui/graphpaper.ico --add-data 'ui;ui' --add-data 'vendor;vendor' --collect-all webview --collect-all pythonnet --collect-all clr_loader scripts/desktop_entry.py +python -m PyInstaller --noconfirm --clean --noupx --windowed --onedir --name GraphPaper --paths . --additional-hooks-dir scripts/hooks --icon ui/graphpaper.ico --add-data 'ui;ui' --add-data 'vendor;vendor' --collect-all webview --collect-all pythonnet --collect-all clr_loader scripts/desktop_entry.py if ($LASTEXITCODE -ne 0) { throw 'Windows desktop build failed.' } python scripts/runtime_manifest.py --verify dist/GraphPaper/_internal if ($LASTEXITCODE -ne 0) { throw 'Frozen runtime checksum verification failed.' } From b0361da25d81f14b54c1a1e682b746c315a290d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:30:42 +0000 Subject: [PATCH 5/9] Refresh System One source publishing manifest --- publish-manifest.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/publish-manifest.json b/publish-manifest.json index 8d0da58..77dfac1 100644 --- a/publish-manifest.json +++ b/publish-manifest.json @@ -3,14 +3,12 @@ "files": { ".gitattributes": "acdb1038616f95b335ceb851c161a6139bf23561b2a74894a9ccb33ded5e71f4", ".github/topics.json": "341aa983b1829624dec7ab7cd2e83eed08d45bc78305033c1e860631c1227e41", - ".github/workflows/apply-runtime-fix.yml": "c4e52e7e6d8e6e69e853716515e2d7a89abb3cb0ea4267ff8e5579c840291ddf", ".github/workflows/docs.yml": "839af4553785c5a2b04a2647791f4056ce7bdb97d20cb5e56bddd65e0d429c68", - ".github/workflows/graph-studio-release.yml": "f4e3ff44a859e65eb0ed9b4b5094a3011e1d00594c3ea94181ec88f66afa04aa", + ".github/workflows/graph-studio-release.yml": "fa40e22ae180fcfb4cb4e0484059c8946808dc6d56d88ae4be7241ad67b1fa65", ".github/workflows/import-build.yml": "c56a990b97242f154907f76059f5b859448ca8de7b582928d63bb3717ac0d60a", ".github/workflows/native-diagnostics.yml": "818584858c3f0dce8ce17ff0ccff9b5984fab6c6d2a1481baa280f841c8cbf53", ".github/workflows/native-probe.yml": "b7e6f3ea3f8fef9e3725ac2a242b7c4be2545bc4491be5ce2e68f723c302bc56", ".github/workflows/quality.yml": "d7da2adb2caf8fb97c58f64ce2dc1025c2958c2c6672c0b7accb9e4556131654", - ".github/workflows/runtime-diagnostics.yml": "1fcd8dd071a78c8a23de790447266f9c51feb5cd6ce27dbfc3587f358227d0c6", ".github/workflows/voice-manifest.yml": "19647138870a09bbe7835df34ddab4017a91da9c1937444a32f3a35b9ef1fa88", ".github/workflows/windows.yml": "65512954b719620063384ce93950b89c9b0aaac2ad6c7c620d505f8787b0444a", ".gitignore": "229a84e2b07c91b31c30f06df236ea17fe211eb75b217adc2eecb3f9d377f9bf", @@ -217,7 +215,7 @@ "requirements-dev.txt": "40b197f9671e3c6db4d47ca73d9679eabafe662a56d4824c6f9e64255e47a5ce", "requirements-graphify.txt": "9eea3e6ea7fa3ceb1247518901d41d55a0813dc5133475172541c68c122454f1", "requirements.txt": "4e73f195c8a6d32b841b671200bf0dcaa91e5a042c585e935e3132ee5e4a02a3", - "scripts/build_release.ps1": "c0be03d1862bc4623e5bc0ce2d7562f0c448f2d5dea1f87d1cd03bdb85fa0323", + "scripts/build_release.ps1": "306a75f19fc5da63701487b9414c5cf0164679521d1953aa584cca069b013c8c", "scripts/build_windows.ps1": "44e0008ff8221097ead7355aa184a60e824d2fcbf0c77e54c74ddfc8f3a54858", "scripts/bundle_codex.py": "651d08e26949dbda47bca14dbc368d97b82e11a28759792cb6096adcea174efa", "scripts/check_codex.py": "2fff4a6cc80188d54914086ab3f3d7d3cf1ec02001541bad6bc8a426fc1bbc81", From 1f1dc11dd03093e06490083dee61a04f3ab073a1 Mon Sep 17 00:00:00 2001 From: Aron Bijl Date: Wed, 14 Oct 2026 13:34:59 +0200 Subject: [PATCH 6/9] Preserve Internet-zone test preconditions after Windows file truncation --- tests/test_windows_runtime.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_windows_runtime.py b/tests/test_windows_runtime.py index 27fc615..a238c54 100644 --- a/tests/test_windows_runtime.py +++ b/tests/test_windows_runtime.py @@ -50,7 +50,9 @@ def test_unmarked_runtime_does_not_prompt(tmp_path): def test_corrupt_assembly_is_not_unblocked(tmp_path): - mod=runtime();root,path,hashes=bundle(tmp_path);tag(path).write_text('ZoneId=3');path.write_bytes(b'changed') + mod=runtime();root,path,hashes=bundle(tmp_path);path.write_bytes(b'changed');tag(path).write_text('ZoneId=3') + # Truncating the primary stream can remove Windows ADS: mark the replaced fixture afterward. + assert tag(path).exists() with pytest.raises(mod.RuntimePackageError,match='match'):mod.repair_bundle(root,hashes,lambda _:pytest.fail('Do not ask to trust mismatched files')) assert tag(path).exists() @@ -72,7 +74,11 @@ def test_symlink_not_followed(tmp_path): def test_changed_during_consent_is_rechecked(tmp_path): mod=runtime();root,path,hashes=bundle(tmp_path);tag(path).write_text('ZoneId=3') - def consent(_):path.write_bytes(b'changed while dialog was open');return True + def consent(_): + path.write_bytes(b'changed while dialog was open') + tag(path).write_text('ZoneId=3') + assert tag(path).exists() + return True with pytest.raises(mod.RuntimePackageError,match='match'):mod.repair_bundle(root,hashes,consent) assert tag(path).exists() From 17db1b67f8b58c68f9b8ed2e70b6de6aa4885b21 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:35:06 +0000 Subject: [PATCH 7/9] Refresh System One source publishing manifest --- publish-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish-manifest.json b/publish-manifest.json index 77dfac1..3716389 100644 --- a/publish-manifest.json +++ b/publish-manifest.json @@ -285,7 +285,7 @@ "tests/test_voice_integration.py": "63c9e4d4dadba187d36169593868af44a4df94896ae1dc06b8b4f451ac0f8eab", "tests/test_voice_library.py": "12071c049d7ccca7d10d531879ed67c19eee97178c0a5c3bc34559aed77417e7", "tests/test_windowless.py": "421620004c87cf24dec771966602b0666e75ecca67fec05071cf40e4b6887f44", - "tests/test_windows_runtime.py": "db61429d4e86741a9d0b636d8ad64aae0f57c35b2d4efb097182fd52a5fc20d7", + "tests/test_windows_runtime.py": "9406f3cbbc8f2f0db03fe4ad340c559353a5d5b1a04c039e439851bdd782e8b5", "ui/app.js": "4f5681c432b8fa5923fa68d57b9ace55cd493ee43be5971316a632f73ec2917d", "ui/assets/CREDITS.md": "e9672e1c683f7db3e5265c52ea98fda9e8eec260e0a251ffa3065e85ebcffd77", "ui/assets/graph-paper.json": "0b37ff27703f8f6adfcd4d05037a594d526749d9641cb6cd226efe1dcc09f687", From 2f8e4356190df226d9d4b546970d54de01ffbc89 Mon Sep 17 00:00:00 2001 From: Aron Bijl Date: Wed, 14 Oct 2026 13:46:21 +0200 Subject: [PATCH 8/9] Resolve Windows temporary-directory aliases in downloaded archive validation --- scripts/check_downloaded_release.py | 21 +++++++++++----- tests/test_downloaded_archive.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 tests/test_downloaded_archive.py diff --git a/scripts/check_downloaded_release.py b/scripts/check_downloaded_release.py index d75faf4..d5c620b 100644 --- a/scripts/check_downloaded_release.py +++ b/scripts/check_downloaded_release.py @@ -3,13 +3,26 @@ import hashlib import json import os -from pathlib import Path +from pathlib import Path, PureWindowsPath import subprocess import sys import tempfile import zipfile +def extract_archive(archive: Path, target: Path) -> Path: + # Windows TEMP may use an 8.3 alias (RUNNER~1). Compare canonical paths on both sides. + target = target.resolve() + with zipfile.ZipFile(archive) as bundle: + for entry in bundle.infolist(): + name = entry.filename + if (PureWindowsPath(name).drive or name.startswith('/') or '\\' in name + or not (target / name).resolve().is_relative_to(target)): + raise ValueError('Unsafe ZIP entry: ' + name) + bundle.extractall(target) + return target + + def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('archive',type=Path) @@ -20,11 +33,7 @@ def main(): archive=args.archive.resolve();out=args.out.resolve();out.mkdir(parents=True,exist_ok=True) with archive.open('rb') as stream:digest=hashlib.file_digest(stream,'sha256').hexdigest() with tempfile.TemporaryDirectory(prefix='GraphPaper-downloaded-') as temporary: - target=Path(temporary) - with zipfile.ZipFile(archive) as bundle: - for entry in bundle.infolist(): - if not (target/entry.filename).resolve().is_relative_to(target):raise ValueError('Unsafe ZIP entry') - bundle.extractall(target) + target=extract_archive(archive,Path(temporary)) executable=target/'GraphPaper/GraphPaper.exe' assert executable.is_file() subprocess.run([sys.executable,str(Path(__file__).with_name('native_smoke.py')), diff --git a/tests/test_downloaded_archive.py b/tests/test_downloaded_archive.py new file mode 100644 index 0000000..8cc1749 --- /dev/null +++ b/tests/test_downloaded_archive.py @@ -0,0 +1,39 @@ +"""The downloaded-build gate resolves its extraction root before containment checks.""" +from pathlib import Path +import zipfile +import pytest +from scripts import check_downloaded_release as checker + + +def extract(archive,target): + assert hasattr(checker,'extract_archive'), 'Safe fresh-archive extraction helper is missing' + return checker.extract_archive(archive,target) + + +def archive_at(tmp_path,name): + archive=tmp_path/'package.zip' + with zipfile.ZipFile(archive,'w') as z:z.writestr(name,b'fixture') + return archive + + +def test_regular_archive_extracts(tmp_path): + archive=archive_at(tmp_path,'GraphPaper/GraphPaper.exe') + target=tmp_path/'unpacked';target.mkdir() + root=extract(archive,target) + assert root==target.resolve() and (root/'GraphPaper/GraphPaper.exe').read_bytes()==b'fixture' + + +def test_aliased_root_is_resolved_on_both_sides(tmp_path): + target=tmp_path/'real';target.mkdir();alias=tmp_path/'alias' + try:alias.symlink_to(target,target_is_directory=True) + except OSError:pytest.skip('Account cannot create directory aliases') + archive=archive_at(tmp_path,'GraphPaper/GraphPaper.exe') + root=extract(archive,alias) + assert root==target.resolve() and (root/'GraphPaper/GraphPaper.exe').is_file() + + +@pytest.mark.parametrize('name',['../escaped.txt','C:/escaped.txt','/escaped.txt']) +def test_unsafe_archive_rejected_before_extraction(tmp_path,name): + archive=archive_at(tmp_path,name);target=tmp_path/'unpacked';target.mkdir() + with pytest.raises(ValueError,match='Unsafe ZIP entry'):extract(archive,target) + assert not list(target.iterdir()) From 1e04b48996f371f9dd54fcca820bd9ffe1eb6f36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:46:46 +0000 Subject: [PATCH 9/9] Refresh System One source publishing manifest --- publish-manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/publish-manifest.json b/publish-manifest.json index 3716389..541d6f2 100644 --- a/publish-manifest.json +++ b/publish-manifest.json @@ -219,7 +219,7 @@ "scripts/build_windows.ps1": "44e0008ff8221097ead7355aa184a60e824d2fcbf0c77e54c74ddfc8f3a54858", "scripts/bundle_codex.py": "651d08e26949dbda47bca14dbc368d97b82e11a28759792cb6096adcea174efa", "scripts/check_codex.py": "2fff4a6cc80188d54914086ab3f3d7d3cf1ec02001541bad6bc8a426fc1bbc81", - "scripts/check_downloaded_release.py": "70b4091f65839262d343c28cd03396ea577dfd89ff8ba9c403db3fbbedeb9ba5", + "scripts/check_downloaded_release.py": "5e01748024d1ababe75707b3cf3db3cf5c67c811127d92c63b72db042f4eb318", "scripts/check_graphify.py": "355f5f04011d4bb12778de9d2c18a654a8f3d4b1bcdbb353cc1e028c93950968", "scripts/check_package.py": "ef863315ea3f21a8f94b412276849df73d1b4fb0aaa30d7890a62a426a6cc4b7", "scripts/check_polemic_live.py": "7dede3422b5daddbf52ecce311a45ddd10307a7e19ad7eef5dc42d22dc839960", @@ -260,6 +260,7 @@ "tests/test_core.py": "167baaef162cfb6212e70fd6a1354479ab31352b4ed1000281a8773260d5a6d5", "tests/test_desktop_and_publish.py": "71fae513bac9965c146b20035b84d006cfb80045fb521523cbff38a11ef37edc", "tests/test_documentation_tools.py": "37c0f4df10a6f2919bb01e1c06813de4efae4a941908a20b3489967f36043c6b", + "tests/test_downloaded_archive.py": "e9952e799bfaee0d9221011788cba1f2378dbe37c44f82ec3574afb877a07bfc", "tests/test_editorial_intent.py": "e14b9943ea285e4f8b9f9c3fcf4fbcf9b8a6ef318c9b35bbaeb54f30d03b15d8", "tests/test_graph_editing.py": "c5cbf6a1de1a993e6c47a7ec63d5882b04d22777792f0cc205d683fcfb01b88d", "tests/test_graphify_extraction.py": "d7da1549de893fda3606da1deea3aa1c53d28e92d210261fbe65418cda9090c4",