Files
tutti/docs/architecture/windows-platform-support.md
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

447 lines
26 KiB
Markdown

# Windows Platform Support
This document defines how Tutti adds Microsoft Windows support without making
the product core depend on Windows implementations. It describes the current
architecture and promotion boundaries; command-level Workspace App rules live
in [Workspace App Runtime](../conventions/workspace-app-runtime.md), and release
mechanics live in [Desktop Release](../conventions/desktop-release.md).
`desktop-windows.md` describes Electron product windows. It is not the source of
truth for Microsoft Windows operating-system support.
## Goals And Current Status
The target is one product behavior across macOS and Windows, with operating
system differences behind capability interfaces. The current Windows target is
Windows 10/11 x64. Windows packaging remains Alpha-only and is not part of the
formal `latest` release path.
Current implementation and evidence:
| Area | Shared owner | Windows boundary | Status |
| ------------------------ | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Desktop daemon lifecycle | Electron main process | packages `tuttid.exe` and injects native resource paths | Windows Alpha CI packages it |
| Workspace Apps | daemon app lifecycle, health, state, and events | `AppShellAdapter` invokes the packaged managed POSIX shell | Onboarding fat package is exercised in Windows Alpha CI |
| Terminal | terminal service and shared terminal contracts | `TerminalProcessFactory` uses ConPTY | focused adapter and daemon WebSocket tests run in Windows daemon-adapter and Alpha CI |
| Agent processes | provider-neutral agent/runtime services | build-tagged executable, command, process handling, and user PATH publication | focused Windows tests run in Agent adapter and Alpha CI |
| Browser | browser service contract | focused Windows executable/profile path behavior | focused Windows tests exist; full browser E2E remains a promotion gate |
| Computer use | computer service contract | Cua Driver 0.18.0 doctor/MCP boundary and owned daemon | focused Windows tests and opt-in MCP smoke exist; screenshot/input E2E remains a promotion gate |
| Files | workspace file APIs and portable Go filesystem behavior | add a narrow adapter only where Windows semantics differ | full Windows Files E2E remains a promotion gate |
| Release | desktop release policy | Certum Authenticode-signed NSIS plus a separately gated Store AppX artifact | signed Direct release automation exists; real-machine acceptance remains a promotion gate |
Passing `windows-latest` CI proves the build and automated paths above. It does
not by itself prove the supported Windows 10 floor, installer UX, upgrade, or
uninstall behavior on real machines.
## Architecture Rules
Windows compatibility is a continuous repository requirement, not a feature
that is considered only when a task explicitly mentions Windows. Every behavior
change must assess its Windows impact. Changes involving paths, filesystems,
temporary storage, executables, commands, shells, environments, processes,
permissions, symlinks, sockets, packaging, or native dependencies are
platform-sensitive by default and require Windows and POSIX reasoning and
focused coverage.
Platform-neutral services depend on capabilities, not operating-system
implementations:
```text
Desktop package and composition
|
v
tuttid common core
lifecycle / API / state / DB / events
|
v
capability interfaces
/ \
Darwin implementations Windows implementations
shell / ConPTY / process / paths
```
Apply these rules when adding another Windows capability or another operating
system:
1. Keep lifecycle, API contracts, persistence, health checks, and event
publication in shared owners.
2. Define the smallest capability interface at the consumer boundary. The
consumer must not import the native library or desktop packaging layout.
3. Select an implementation in the owning composition root or a small
build-tagged factory. OS checks do not belong throughout business logic.
4. Keep portable behavior shared. Do not introduce `FooAdapter` merely because
a module can run on multiple operating systems.
5. Group cohesive platform behavior. A platform does not need a separate file
for every helper; split only where build constraints or genuinely different
semantics require it.
6. Derive paths from injected roots and standard platform APIs. Do not hardcode
drive letters, user directories, installation locations, or executable
search results.
7. Treat every path crossing a process, RPC, JSON, environment, or provider
boundary as a host path unless the contract explicitly declares a virtual
namespace. Construct host paths with the platform path API, preserve the
required absolute-path base such as `cwd`, and never send a POSIX-rooted
literal such as `/tmp` or `/sandbox-tmp` to a Windows parser.
8. Resolve executables through the owning adapter. Account for `.exe`, `.cmd`,
and `.ps1`, PATHEXT and PATH behavior, spaces and non-ASCII characters in
paths, and Windows command-line quoting. Do not assemble shell command
strings when an argv-based process API is available.
9. Tests for a platform-sensitive contract must exercise the receiving parser,
process, or filesystem boundary on Windows where practical. A mock that only
verifies emitted strings or serialized maps is insufficient evidence of
Windows compatibility.
This is dependency inversion at the native boundary, not a requirement to
create parallel copies of each service.
### Workspace project identity and imported rail placement
Project paths have one display form and one comparison form; callers must not
compare raw strings when the path crosses a desktop/daemon boundary. The shared
user-project core normalizes slash direction and trailing separators, then
folds case only for Windows-shaped drive or UNC paths. POSIX paths remain
case-sensitive. On a Windows host, a single-root POSIX project path is an
explicit logical namespace: the Go rail store cleans it with POSIX semantics
and never rebases it onto the current drive. Native drive and UNC paths keep
Windows filesystem normalization. The rail store keeps the resulting canonical
path for display and derives a stable section key for persistence. Project
registration and deletion resolve an incoming path variant to the existing
stored row before applying the table's ordinary unique-path write guard, so no
second identity column is needed.
External session import can persist a session before its selected project is
registered. The import path therefore registers projects and repairs only
sessions carrying the durable `imported` marker in the same workspace SQLite
transaction. Startup migration `workspace_agent_activity_rail_v2` replays the
same repair for historical rows; ordinary conversations are not moved. This
uses existing APIs and tables—no new wire fields or database columns are
required.
## Workspace App Data Flow
Workspace Apps keep one manifest and one POSIX `bootstrap.sh` across platforms:
```text
App Center install
-> validate and unpack one fat package
-> AppRunner owns lifecycle and environment
-> AppShellAdapter selects host invocation
-> bootstrap.sh reads TUTTI_PLATFORM
-> bin/windows-amd64/*.exe or bin/darwin-*/*
-> health check
-> state persistence and business events
```
On packaged Windows Desktop, the desktop vendors a pinned minimal POSIX shell
runtime, currently implemented with Bash, and injects its absolute path when
starting `tuttid.exe`. The Windows
`AppShellAdapter` invokes that Bash with profiles disabled and exposes its
managed command directory through `PATH`. Apps do not depend on Git for Windows
or WSL, and the manifest does not duplicate platform entrypoints.
Windows direct development uses the same managed runtime. The desktop `predev`
hook prepares `apps/desktop/build/managed-posix-shell`, and the Electron main
process resolves that runtime when it builds the daemon launch environment.
Therefore `pnpm dev` does not depend on a caller remembering a separate shell
export; the daemon still receives `TUTTI_MANAGED_POSIX_SHELL` before it starts.
Windows staging and E2E acceptance should continue to use the dedicated E2E
launcher because it also owns state isolation, cleanup, and readiness checks.
The package is fat because it contains every currently shipped platform
artifact. `TUTTI_PLATFORM` selects the artifact at runtime. Adding a future
platform key should add an artifact and build job, not another app lifecycle.
The desktop also resolves the packaged or development native `tutti.exe` at
composition time and passes its absolute path to `tuttid`. The Workspace App
runner exposes that executable as `TUTTI_CLI` together with the effective
`TUTTID_LISTENER_INFO_PATH`. Apps therefore launch the CLI without `cmd.exe`
or batch-file parsing. The user-facing `tutti.cmd` remains a terminal PATH shim;
it is not the Windows Workspace App execution contract.
## Native Adapter Boundaries
### Workspace App shell
`AppRunner` owns app lifecycle and depends on `AppShellAdapter`. The adapter
validates host script semantics, answers how to invoke a package script, and
returns the managed command directory that must be available. Production wiring
creates one platform adapter and injects it into App Center, AppRunner, and App
Factory; those consumers do not branch on the operating system. The reusable
packaged resource is named
`managed-posix-shell`; shell download, verification, and desktop resource
layout remain desktop packaging concerns. Other domains may reuse that resource
through their own narrow adapter, but must not depend on `AppShellAdapter` or
expand the Workspace App contract.
### Terminal
The terminal service depends on `TerminalProcessFactory` and `TerminalProcess`.
The factory supplies both the default `TerminalShellSpec` and process creation.
Unix PTY file descriptors stay on a Unix-private capability and are not part of
the shared process contract; Windows therefore does not provide a fake FD.
Unix PTY and Windows ConPTY libraries remain private to their implementations.
Session lifecycle, resize semantics, snapshots, WebSocket transport, and exit
events remain shared daemon behavior.
### Agent process execution
Agent installation and launch keep provider/product policy shared. Windows
command resolution, executable verification, npm launcher layout, and process
creation stay in the runtime command/process boundary. Managed npm installs use
the selected user executable directory (normally `%USERPROFILE%\.local\bin`)
as npm's Windows prefix, because Windows npm writes `.cmd`/`.ps1` launchers
directly under the prefix. After a successful managed install/update, the
daemon's narrow Windows user-path adapter idempotently appends that directory
to `HKCU\Environment\Path` and broadcasts `WM_SETTINGCHANGE`; it never
writes the machine-wide path or changes Unix/macOS shell profiles. Do not make
provider installers assemble shell command strings to handle Windows.
Fresh installs use `%USERPROFILE%\.local\bin`. The resolver also scans the
older `%USERPROFILE%\.local` npm prefix after the current directory, so an
existing verified package is reused and updated in place instead of creating a
second copy. After verification the daemon publishes the directory that owns
the selected launcher. It does not migrate or delete the legacy package.
Managed Agent Extensions and the provisioned Claude Code runtime publish into
the same `%USERPROFILE%\.local\bin` contract. Their versioned executables stay
under `%USERPROFILE%\.local\share\tutti\agent-runtimes`; a stable per-Agent
`.cmd` launcher and a user-level `.cmd` launcher form two verified hops to the
active executable. This avoids file-symlink privilege and keeps versioned
runtime directories out of `PATH`. The daemon refuses to replace an existing
entry unless it carries the Tutti launcher marker and points inside the
expected managed runtime root. Successful install actions surface user-PATH
write failures, while status-time adoption repairs PATH on a best-effort basis.
Registry changes affect new processes only, so an already-open terminal must be
restarted before it can resolve a newly published command.
Portable Agent Extension manifests keep npm and pnpm launch executable names
extensionless. The Windows install-plan adapter resolves those names to the
actual `.cmd` launcher before verification, activation, version probing, and
launch; the structured command adapter invokes the launcher through `cmd.exe`.
Product and provider code must not append the suffix or assemble the shell
command itself.
Managed-runtime adoption must also release the verified source directory handle
before renaming that directory. After rename, reopen the promoted directory and
repeat the fingerprint/integrity check; rollback follows the same close-rename-
reopen order. Unix permits rename with the old handle open, but Windows rejects
it as a sharing violation.
Provider-owned account-usage helpers do not execute npm `.cmd` launchers or
copy JavaScript into a fake `.exe`. Their optional package is installed and
activated separately from the ACP runtime. The process boundary verifies the
fixed `node.exe` interpreter and the declared CommonJS script independently,
then feeds the verified script bytes to Node. Windows CI must exercise an
actual npm pack/install and account-usage probe in addition to native Go
executable verification; a companion failure may produce only
`runtime_unavailable`, never a not-installed Agent. Companion installation is a
daemon-owned reconciler with durable restart recovery and bounded retry backoff; setup
status reads never initiate its package download.
The first probe builds a content-addressed Node snapshot with context-aware
copy/hash. Later probes reuse the read-locked snapshot without copying or
hashing `node.exe` again; a restarted daemon verifies it once before reuse.
Extension session-home preparation keeps its source declaration portable. An
explicit source environment variable wins; otherwise the Windows adapter maps a
leading-dot top-level directory to the native user cache root
(`%LOCALAPPDATA%`) before the shared resolver considers a migrated literal
user-home-relative directory. Provider IDs and Windows path literals must not
leak into extension or Agent lifecycle policy.
System proxy resolution follows the same shared precedence on macOS and
Windows: session/process environment, then the operating-system static proxy,
then direct connection. The common resolver owns merging, `NO_PROXY`, caching,
diagnostics, and the `TUTTI_DISABLE_PROXY_AUTODETECT` escape hatch. Build-tagged
adapters only read native state: Darwin uses `scutil --proxy`; Windows reads the
current-user WinINet `Internet Settings` values. Windows protocol maps and
bypass entries are normalized to `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`.
PAC execution and SOCKS conversion remain out of scope because the daemon does
not embed a PAC/SOCKS engine. WinINet bypass entries that cannot be represented
faithfully by standard `NO_PROXY` syntax (for example `10.*` and `<local>`) are
not projected; loopback, `.local`, exact hosts, and `*.domain` suffixes remain
covered.
Agent Target setup uses one Desktop-owned, version-pinned `uv` resource rather
than downloading `uv` separately for each Python agent. Release staging copies
the official compressed archive for each packaged architecture under
`bin/managed-uv/<platform>/<version>/`; both staging and tuttid verify the
pinned size and SHA-256. Electron injects only the resource root through
`TUTTI_BUNDLED_UV_ROOT`. The daemon keeps extraction, cache markers, and the
dynamic official-download fallback. Node, Python, Kimi, Hermes, and other agent
payloads remain dynamically installed, so this adds roughly one compressed uv
archive per architecture rather than a complete cross-language toolchain.
The signed macOS release is the one packaging exception. The upstream macOS
archives contain nested `uv` and `uvx` executables without Tutti's Developer ID
signature, so Apple notarization rejects the archive when it is embedded in the
app. macOS therefore uses the same pinned, checksum-verified dynamic download
fallback at first use; Windows and Linux continue to ship their bundled uv
archive.
For generic ACP providers such as OpenCode, provider resolution rewrites the
adapter command once and every status, login, model-catalog, and session path
uses that resolved executable. On Windows a complete isolated npm package is
preferred over an earlier stale PATH shim; package metadata and containment are
validated before its declared binary is accepted.
The Windows Desktop package also vendors the pinned Mutagen executable used
when file-symlink creation is unavailable. Electron resolves the packaged
resource and injects `TUTTI_MUTAGEN_BIN` into `tuttid`; Workspace Apps inherit
that same absolute path. Release staging downloads the official archive and
license texts, verifies their pinned SHA-256 digests, and packages only the
Windows amd64 executable and notices. Runtime execution therefore has no
Mutagen download dependency, while an explicit `TUTTI_MUTAGEN_BIN` remains an
operator override. If the packaged or configured executable is unavailable,
runtime preparation uses a guarded per-run auth copy and copies a valid
refreshed credential back only when the stable source has not changed
concurrently.
### Browser and Files
Browser and Files do not require broad platform interfaces by default. Keep API,
state, validation, and orchestration shared. Introduce or extend a narrow
adapter only after a real difference is identified, such as browser executable
discovery, profile location, process termination, path containment, atomic
move, or timestamp behavior. Prefer standard Go and Electron APIs when they
already provide equivalent semantics.
### Computer use
Computer use keeps the Tutti MCP/tool policy and session lifecycle shared. On
Windows, the narrow adapter invokes the installed Cua Driver 0.18.0 binary,
uses its read-only `doctor --json` probe for readiness, and lazily owns a local
`serve` process while the computer service is active. The desktop resolves the
official per-user install locations and passes an explicit entry path when it
starts a new `tuttid`; the daemon also resolves those locations so an install
performed while the current desktop process is running is visible without a
restart. The native UI Automation, capture, and input implementation remains
inside Cua Driver rather than becoming a Tutti platform library.
Daemon startup readiness is bounded but tolerant of transient status failures.
If the service still owns a live `serve` process, a later readiness check polls
again instead of failing immediately. A process exit before readiness and a
readiness timeout remain distinct, sanitized diagnostic errors.
The desktop does not currently vendor `cua-driver.exe` into the Windows
package. Users or deployment tooling must install the pinned driver (or set
`TUTTI_COMPUTER_MCP_ENTRY_PATH`); packaging a signed helper is a separate
promotion decision. Updating the driver version requires rerunning the doctor,
MCP contract, and real screenshot/input gates before changing the pin.
## Packaging And Release Boundary
The Alpha workflow is intentionally separate from the formal desktop release:
- `.github/workflows/windows-agent-adapters.yml` and
`.github/workflows/windows-daemon-adapters.yml` provide focused pull-request
coverage and maintain reusable caches on matching `main` pushes; they do not
produce desktop packages;
- `.github/workflows/windows-desktop-alpha.yml` always tests Windows x64 and
builds the Desktop bundles for pull requests;
- pull requests build, smoke-test, and upload an unsigned NSIS installer only
when Desktop packaging inputs change; manual runs always produce the
installer;
- the workflow does not publish a GitHub Release or mutate stable/prerelease
update metadata;
- `.github/workflows/desktop-release.yml` signs every non-dry-run Windows
package with Certum SimplySign, independently verifies the installer,
embedded uninstaller, and internal EXEs on Windows, then stages Windows
beside macOS assets.
Windows may enter a formal Beta/RC or stable channel only after all applicable
gates below pass:
- Windows 10 and Windows 11 x64 install, launch, upgrade, rollback, and uninstall
verification on real or cloud machines;
- Onboarding, Browser, Files, Terminal, and AgentGUI end-to-end verification;
- successful Authenticode smoke and formal-package runs using the organization-
owned Certum SimplySign configuration;
- managed Bash license, notice, and corresponding-source distribution review;
- crash/log collection and release-asset traceability;
- formal release staging, updater metadata, mirror, notification, and rollback
tests.
Automated signing is one release boundary, not proof of the remaining
real-machine gates. Public channel enablement must continue to follow the
release policy and its protected stable-candidate approval rather than treating
a successful signing job as complete Windows support evidence.
The stable Microsoft Store path is a separate, opt-in release surface:
- `pnpm --filter @tutti-os/desktop build:win:store` reuses the Windows payload
and emits one x64 AppX submission package;
- `.github/workflows/desktop-store-submit.yml` accepts only a plain stable tag,
verifies that the tag and commit match, validates the package identity,
publisher, four-part version, architecture, and SHA-256, then optionally
submits it with the official Microsoft Store Developer CLI;
- `.github/workflows/desktop-release.yml` calls that workflow only when
`TUTTI_WINDOWS_STORE_SUBMISSION_ENABLED=true`, the publication mode is
`publish`, and Direct promotion has succeeded; the Store job is downstream
from, never a dependency of, Direct staging or promotion;
- the AppX manifest registers the `tutti` login callback protocol and the Store
workflow verifies that registration together with `runFullTrust`;
- RC and beta tags remain Direct-only. A separate Store beta product or package
flight is not part of this implementation.
The first Store submission is a one-time Partner Center operation: upload the
validated AppX artifact and complete the listing, properties, age rating,
pricing, and availability there. The Store Developer CLI does not implement a
first submission from a loose MSIX file. Once that first submission has been
accepted, later stable package updates use the automated Store workflow.
Microsoft Store submission does not make the Direct NSIS installer signed. The
Store package may be treated as a supported public download only after its
Partner Center certification, installation, sidecar, protocol, update, and
uninstall gates have passed on real Windows systems.
## Iteration Path
1. **Alpha foundation:** keep Windows build and automated adapter/Onboarding
checks green in the isolated workflow.
2. **Product-chain validation:** exercise Browser, Files, Terminal, Agent
install/start, AgentGUI launch, persistence, event push, and restart recovery
on Windows 10 and Windows 11 x64.
3. **Store prototype:** use the manual Store workflow with its submission flag
disabled first, then validate a test Partner Center product before replacing
the complete environment profile with production credentials and identity.
4. **Store stable release:** enable automatic stable submission only after
certification and installation gates pass; keep Direct NSIS and Store update
responsibility isolated.
5. **Additional architectures:** add an explicit platform key such as
`windows-arm64`, its native artifacts, and CI coverage. Reuse common services
and existing capability contracts unless ARM64 exposes a genuine new
boundary.
Each phase must preserve the previous macOS path. A phase can be rolled back by
disabling its Windows workflow or publication integration without changing App
manifests or common lifecycle contracts.
## Non-Goals
- shipping a general-purpose Unix environment on Windows;
- requiring Git for Windows, WSL, or a user-installed Bash;
- adding `bootstrap.cmd` or a platform entrypoint matrix to every Workspace App;
- duplicating services, API contracts, or persistence models per platform;
- supporting Windows ARM64 before its binaries, packaging, and test matrix are
explicitly added;
- publishing unsigned Alpha artifacts through stable or `latest` channels.
- publishing RC or beta builds to Microsoft Store in the first Store phase;
- mirroring a Store-signed package back to GitHub Releases or the Direct updater
feed.
## Open Decisions
These items are not established by the current code and must not be presented
as completed support:
- the exact minimum supported Windows 10 release;
- whether Browser and Files are mandatory for the first externally distributed
Alpha, rather than later preview promotion gates;
- Windows ARM64 schedule;
- the final public wording and timing for announcing Windows support.
- the production Partner Center product identity and least-privilege Entra
application ownership;
- the version-controlled repository and staging environment for the
`tutti-desktop-download` Worker;
- the exact Windows 10/11 Store install, update, sidecar, protocol, and data-path
evidence required before enabling production submission.