← Về thư mục
📄 / / profiles / r-and-d / raw_evidence / OpenAI4S / docs / platforms.md

Supported platforms

The frozen matrix (v02-decisions.md, 8.5). This page says what the code enforces today, and names the gates that are not yet met — a support claim nobody has to take on faith is the only kind worth publishing.

Platform Tier Kernel OS sandbox Gate
macOS (Apple Silicon) stable runs Seatbelt, enforced and smoke-tested nightly Developer ID signing + notarization via scripts/notarize_macos_dmg.sh; public DMG requires macos_asset=notarized, otherwise omitted. No release has met this gate: v0.3.0 publishes no DMG (D11), and v0.2.0's image was an ad-hoc-signed preview. The tier describes the code; install from PyPI
macOS (Intel) stable runs Seatbelt the .dmg is Apple Silicon only; install from PyPI
Linux (x86_64 / arm64) beta runs bubblewrap, enforced full filesystem/egress boundary runs in every CI (harness/smoke/linux_sandbox.py); private-PID Python/R interrupt is a separate job that allows raw networking; the release workflow does not re-execute the full smoke until multiple scheduled greens pass
Windows (native) unsupported refused none exists not planned; use WSL2, which reports as Linux
Anything else unsupported refused — —

What ships, per platform

A support tier is a claim about the code. This is the separate question of what a person can actually download, and the two are not the same thing — the Windows row above says "unsupported" and the Windows row below says a package exists, because that package does not run OpenAI4S on Windows.

Download Built by Verified by What it is
OpenAI4S-<v>-macos-arm64.dmg scripts/build_macos_dmg.sh then scripts/notarize_macos_dmg.sh verify_macos_bundle.py An .app with an embedded relocatable CPython and the pre-baked science stack. A public release only uploads this image when macos_asset=notarized (sign → notarytool → staple); the default is omit. Not published in v0.3.0, because the signing and notary credentials do not exist. The last image, v0.2.0's, is an ad-hoc-signed preview that is still on the v0.2.0 release page.
OpenAI4S-<v>-linux-x86_64.tar.gz scripts/build_linux_bundle.sh verify_linux_bundle.py The same payload as a relocatable directory, plus a .desktop template and a per-user install.sh. Unpack anywhere and run ./OpenAI4S.
OpenAI4S-<v>-windows-x86_64.zip scripts/build_windows_zip.sh verify_windows_zip.py A Windows launcher wrapped around that exact Linux tarball. It requires WSL2 + working bubblewrap 0.8.0+, installs offline, and opens the authenticated URL returned by the WSL CLI. Not a native Windows build; see below and the Windows/WSL2 guide. First published in v0.3.0; the scope that is still unverified is listed in the parity audit.
openai4s-<v>-py3-none-any.whl uv build verify_release_artifacts.py The zero-dependency wheel, for any supported platform with its own Python.
ghcr.io/pku-yuangroup/openai4s:<v> and :latest Dockerfile, pushed by publish-image.yml scripts/container_smoke.sh A linux/amd64 container image of the daemon and workbench, published from 0.2.0 on. publish-image.yml pushes it only after the smoke passes. An anonymous docker pull works only while the GitHub package is public; otherwise run docker build -t openai4s:local . from a checkout. Multi-arch is free here in a way it is not for the bundles — the wheel is py3-none-any and the science stack has manylinux aarch64 wheels — but nothing has built an arm64 image either, so that is untested rather than promised. See docker.md.

Only x86_64 slices are published, the same way the .dmg is Apple Silicon only. Both build scripts take ARCH=aarch64 and produce a correct arm64 bundle, but nothing publishes one yet, so arm64 Linux and Windows-on-ARM install from PyPI (pip install openai4s). Naming an architecture we do not actually upload would put the burden of discovering that on the person downloading it.

The Linux bundle is deliberately not an AppImage. An AppImage is a squashfs mounted through FUSE, and this program's job is to spawn subprocesses under bubblewrap; nesting a user namespace inside a FUSE mount is the combination that fails on hardened and containerised hosts, and it would fail at cell execution time — long after the app looked like it had started successfully.

Python versions

Version requires-python Classifier CI offline suite Ships in the .dmg Ships in the container
3.10 admitted (the floor) yes yes no no
3.11 admitted yes no — see below no no
3.12 admitted yes yes (+ science + chemistry extras) no no
3.13 admitted yes yes yes no
3.14 admitted yes yes no yes
3.15+ admitted by >=3.10 no no no no

Three files used to each claim something different, and the disagreement was invisible because nothing compared them. requires-python said >=3.10, the classifiers stopped at 3.12, CI ran 3.10 and 3.12 — and build_macos_dmg.sh embedded 3.13. So the build that reaches the most end users, the double-clickable one, ran on the single interpreter nothing in the repository exercised, on a version the package did not claim to support. A 3.13-only failure would have shipped green, because no job could see it.

3.13 and 3.14 are now classified and tested. The reconciliation is enforced by tests/test_platform_support.py, which reads the package metadata, CI workflow, bundle scripts, and Dockerfile rather than restating them: a matrix written down in prose is correct on the day it is written.

3.11 is claimed and not directly tested, on purpose. CI runs the floor (3.10), the .dmg interpreter (3.13), the container interpreter (3.14), and 3.12 with the optional science and chemistry extras. A version bracketed on both sides by tested ones is a different risk from one outside the tested range entirely. Naming the gap is the point — it is a stated cost, not an oversight, and the test that enforces the rest deliberately does not enforce this.

3.15 and later are admitted by >=3.10 and are not claimed. The bound is left open rather than capped so a new interpreter does not block installation, but nothing here has run on one.

What "unsupported" means here

It means the kernel refuses to start, not that it prints a warning and tries anyway. Before this, a native Windows install printed one line during onboarding and then went on to spawn a kernel — and a program that warns and proceeds has made a different promise from one that refuses. The first leaves a scientist to discover the problem from a half-working analysis, which is precisely the failure a product built on trustworthy results cannot afford.

The refusal lives at the kernel spawn path (openai4s/platform_support.py), which every Python and R kernel passes through, so there is no route to a subprocess that skips it. The message names both the reason (POSIX subprocesses, and no Windows sandbox backend) and the way out (WSL2).

The Windows package, and why it is not a contradiction

There is a Windows download, and native Windows is still refused. Both are true because the package does not run OpenAI4S on Windows: it is a launcher that installs the Linux bundle into the user's WSL2 distribution, starts the daemon there, asks openai4s url for the authenticated first-visit URL, and opens the Windows browser at the forwarded localhost port. WSL2 reports as linux, so what runs is the supported build, unmodified.

Making the documented way out double-clickable is the whole value. The alternative — shipping something that starts on native Windows — would move the "warns and proceeds" failure from the kernel to the installer, where it is harder to see and lands on a machine nobody here can debug.

Three properties keep the package honest, and scripts/verify_windows_zip.py fails the build on each:

Why Linux is beta and macOS is stable

Not a difference in the code — the same kernel and the same host RPC run on both. The tiers differ in what has been proven:

Both smokes check the same four boundaries, from one shared implementation (harness/smoke/sandbox_boundary.py): a cell cannot write outside its workspace, cannot open a socket, can write inside its workspace, and cannot leak the daemon's credentials into a subprocess it spawns. They are shared rather than copied because two copies drift until one platform quietly stops checking what the other still does.

The separate linux_bwrap_interrupt.py smoke is deliberately narrower. It retains team KernelReadIsolation, requires the real --unshare-pid + --info-fd + procfs + pidfd production path, observes Python and R as PID 2, interrupts a long-running Cell, and proves the same kernel executes again. The hosted-runner job sets OPENAI4S_KERNEL_ALLOW_RAW_NETWORK=1, so this is evidence for process identity and SIGINT persistence only—not for the network boundary. The job pins Ubuntu 24.04 and loads its packaged bwrap-userns-restrict AppArmor profile, which allows bwrap to construct the namespace and strips capabilities from the executed worker. It does not disable the runner's host-wide unprivileged-userns restriction.

Why the full Linux boundary smoke is not a release-workflow platform check yet

The full filesystem-and-egress smoke now runs as its own CI job on Ubuntu 24.04, independent of the interrupt job, with the packaged bwrap-userns-restrict AppArmor profile loaded and without OPENAI4S_KERNEL_ALLOW_RAW_NETWORK. The check run is attested at the frozen SHA (ci-linux-sandbox-full). That is not the same as re-executing it inside the release workflow's platform-checks matrix: an unproven matrix leg once made every publication unreachable. The row stays in release_gates.PLATFORM_CHECKS_UNAVAILABLE until multiple scheduled greens plus a candidate SHA have passed.

The interrupt job still deliberately allows raw networking so its process-identity evidence does not depend on network setup; that exception is why a green interrupt check is not this check.

OPENAI4S_KERNEL_SANDBOX=enforce uv run python -m harness.smoke.linux_sandbox

Degraded sandboxes

OPENAI4S_KERNEL_SANDBOX takes auto (default), enforce, or off. On auto, a missing backend degrades visibly — a runtime warning and a machine-readable degraded status — rather than silently. enforce fails closed before a worker starts. The macOS nightly smoke runs under enforce, which is why a missing Seatbelt is a CI failure rather than a shrug.

The container image is the ordinary case of that degradation rather than an exception to it. An unprivileged container cannot give bubblewrap the user, mount and network namespaces it wants, so auto warns once and the container becomes the boundary. (The hosted CI runner is not a container: the linux-sandbox-full job runs bubblewrap directly on the runner, with the packaged bwrap-userns-restrict AppArmor profile loaded.) What that boundary does not replace is specific: an unenforced sandbox drops the secret-read masks over <data_dir>/openai4s.db and <data_dir>/access-token at the same time as the network namespace, and a cell's working directory is two levels below that token. docker.md states the trade in full.