Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ jobs:
args: check

windows-compile:
name: windows-compile
name: windows-preview-checks
runs-on: windows-latest
env:
# Use cgofuse's native Go Windows loader, matching the preview build.
CGO_ENABLED: "0"
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-go@v7.0.0
Expand All @@ -80,7 +83,16 @@ jobs:
cache: true
- name: Compile all test packages without claiming runtime validation
run: go test ./... -run '^$' -count=1
- name: Test Windows adapters, worker lifecycle and tray state
run: go test ./internal/tray ./internal/enroll ./internal/cli ./internal/service ./internal/mountfs ./internal/pack ./internal/dirsync ./internal/sessionns ./internal/vfs -run 'TestMonitor|TestPolicy|TestDashboard|TestHistory|TestIncidents|TestUnreadable|TestPeriodicEnrollment|TestEnrollmentWorker|TestPersistentEnrollment|TestWindows|TestFuse|TestPublishedLogicalBytes' -count=1 -timeout=120s
- name: Test the WinFsp frontend and engine handover
run: go test -tags winfsp ./internal/mountfs -run 'TestWindowsCore|TestWindowsFuseErrorMapping|TestFuse' -count=1 -timeout=120s
- run: go build ./cmd/codexfold
- name: Build Windows mount and native tray
run: |
go build -tags winfsp -o "$env:RUNNER_TEMP/codexfold-winfsp.exe" ./cmd/codexfold
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
go build -ldflags='-H=windowsgui' -o "$env:RUNNER_TEMP/codexfold-tray.exe" ./cmd/codexfold-tray

race:
name: race
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.DS_Store
.worktrees/
/.tmp/
/.tmp-test-windows-runtime/

# Xcode build products and derived data
platform/darwin/fskit/build/
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ The requirements and release gates for normal JSONL paths backed transparently b
|---|---|---|---|
| macOS 27 | Apple-native Swift FSKit | Historical signed build 102 real-client and native-mount matrix; current worktree requalification is partial | `fs-engine-preview` |
| Linux | FUSE3 | Real unprivileged mount, mutation, remount, recovery, performance, and user-service lifecycle | Preview; real Codex client validation remains incomplete |
| Windows | WinFsp | Cross-build and compile coverage | Not runtime-validated |
| Windows | WinFsp, SCM services and native WebView2 tray | Complete local-copy Codex app-server lifecycle, exact-byte restart, persistent worker and guarded live engine replacement on an isolated mount | Tested preview flows; Desktop GUI, reboot and SYSTEM engine-split qualification remain incomplete |

The transparent filesystem preview has these explicit boundaries:

- macOS now targets an Apple-native Swift FSKit extension connected over a versioned Unix-domain-socket protocol to the Go CodexFold daemon. The signed 2026-07-23 build 102 App/extension and its then-current helper historically passed the isolated mounted-operation, exact-byte, cache-coherency, performance, crash/host-restart, rollback, and real Codex CLI/Desktop matrix. That evidence does not automatically transfer to the current worktree candidate.
- Release source metadata is `0.3.0 (103)`. Build 103 compiles and passes nested signature verification, while the completed mounted and real-client matrix remains historical build 102 evidence. Changes in the current worktree require their own candidate attachment, real-client workload, restart, fault-injection, and recovery evidence.
- The earlier synchronous FUSE-T NFS route remains historical validation evidence and a development fallback only. FUSE-T's third-party FSKit backend remains rejected after deterministic byte-loss and cache-invalidation failures; it is not the Apple-native FSKit implementation in this repository.
- Linux FUSE3 has real unprivileged read, append, copy-on-write, truncate, archive rename, crash recovery, remount, performance, and `systemd --user` lifecycle evidence.
- Windows has a WinFsp adapter and native Windows Service host that cross-compile, but no real Windows/WinFsp host has validated them yet.
- The production service and production Codex home remain disabled by default. The current worktree's isolated flow has observed a real Cockpit **Start**, an isolated Desktop/app-server process chain, and an unchanged production Codex process. It has not yet attached the current CodexFold candidate, observed a real Desktop task mutation through that managed route, or completed candidate-only fault injection and recovery; `codexInstanceAcceptanceComplete`, `codexFoldCandidateAcceptanceComplete`, and `realAcceptanceComplete` therefore remain false. Promotion is blocked by that exact evidence matrix, not by a fixed observation period.
- This Windows branch has passed complete local-copy tests with Codex 0.159.2 app-server: resume, history reads, recorder writes, fork, archive/unarchive of a compressed thread, and a separate managed deletion with physical purge. SCM stop/start preserves exact bytes and client access. A persistent SCM folding worker supports pause/resume and fresh progress; the native tray adds policy controls, 30-day aggregate history, incident details and JSON diagnostics. An isolated real WinFsp mount passed storage-engine replacement and failed-candidate rollback while preserving the frontend PID and mount nonce. A legacy installation needs one offline upgrade to enable that split. The local legacy namespace has been activated; the full Desktop GUI, reboot/power loss and SYSTEM engine-split qualification remain unverified. See the [Windows preview guide](docs/windows-preview.md).
- The macOS production service and production Codex home remain disabled by default. The current worktree's isolated macOS flow has observed a real Cockpit **Start**, an isolated Desktop/app-server process chain, and an unchanged production Codex process. It has not yet attached the current CodexFold candidate, observed a real Desktop task mutation through that managed route, or completed candidate-only fault injection and recovery; `codexInstanceAcceptanceComplete`, `codexFoldCandidateAcceptanceComplete`, and `realAcceptanceComplete` therefore remain false. Promotion is blocked by that exact evidence matrix, not by a fixed observation period.
- CodexFold recovery operates only on CodexFold-owned components. It never quits, restarts, signals, or reopens a running Codex process. On macOS the backend, supervisor, and incident helper are resident; the menu-bar App is restarted after a crash but an explicit user Quit remains a quit.
- The native macOS menu-bar App shows current health, measured space saved, current read/write speed, compact trends, selectable 1-hour/24-hour/7-day/30-day history, and the timeline of failures that lasted at least ten seconds. A standalone menu-bar launch that has never observed or registered the file-service runtime shows an unconnected state instead of reporting a critical incident. History stores only timestamps, health, counts, byte totals, and aggregate transfer rates, never session content.
- The current worktree validation has not installed, updated, stopped, restarted, or signaled the production CodexFold service, FSKit App/extension, or LaunchAgent set. Any exact production target requires a new explicit user authorization before an apply or lifecycle operation.
- The current macOS worktree validation has not installed, updated, stopped, restarted, or signaled the production CodexFold service, FSKit App/extension, or LaunchAgent set. Any exact production target requires explicit user authorization before an apply or lifecycle operation.

See [the Linux FUSE3 validation](docs/validation-linux-fuse3.md) and [the macOS canary validation](docs/validation-macos-canary.md) for the evidence boundary. The default build remains storage-only; platform mounts require explicit build tags and installed host prerequisites.

Expand Down
17 changes: 17 additions & 0 deletions cmd/codexfold-tray/assets/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="0.4.0.0" processorArchitecture="*" name="CodexFold.Tray" type="win32"/>
<description>CodexFold</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="asInvoker" uiAccess="false"/></requestedPrivileges></security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/></application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>
Binary file not shown.
Binary file added cmd/codexfold-tray/assets/codexfold.ico
Binary file not shown.
100 changes: 100 additions & 0 deletions cmd/codexfold-tray/assets/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//go:build ignore

// Draw the Windows storage glyph at multiple resolutions without external assets.
// Run from cmd/codexfold-tray: go run assets/generate.go
package main

import (
"bytes"
"encoding/binary"
"image"
"image/color"
"image/png"
"math"
"os"
)

func rounded(x, y, left, top, right, bottom, radius float64) bool {
dx := math.Max(math.Max(left+radius-x, 0), x-(right-radius))
dy := math.Max(math.Max(top+radius-y, 0), y-(bottom-radius))
return x >= left && x <= right && y >= top && y <= bottom && dx*dx+dy*dy <= radius*radius
}

func glyph(size int, warning bool) *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, size, size))
const samples = 4
for y := 0; y < size; y++ {
for x := 0; x < size; x++ {
var r, g, b, a float64
for sy := 0; sy < samples; sy++ {
for sx := 0; sx < samples; sx++ {
px := (float64(x) + (float64(sx)+.5)/samples) * 64 / float64(size)
py := (float64(y) + (float64(sy)+.5)/samples) * 64 / float64(size)
c := color.NRGBA{}
if rounded(px, py, 2, 2, 62, 62, 14) {
c = color.NRGBA{uint8(66 - py*.13), uint8(125 - py*.2), uint8(201 - py*.15), 255}
}
// Stacked sheets flowing into a compact storage drive.
if rounded(px, py, 19, 15, 45, 18, 1.5) || rounded(px, py, 16, 22, 48, 25, 1.5) || rounded(px, py, 12, 30, 52, 49, 4) {
c = color.NRGBA{248, 251, 255, 255}
}
if rounded(px, py, 18, 38, 35, 41, 1.5) || math.Hypot(px-44, py-39.5) <= 1.8 {
c = color.NRGBA{58, 112, 185, 255}
}
if warning && math.Hypot(px-51, py-51) <= 10 {
c = color.NRGBA{255, 255, 255, 255}
if math.Hypot(px-51, py-51) <= 8 {
c = color.NRGBA{208, 142, 39, 255}
}
}
r += float64(c.R) * float64(c.A) / 255
g += float64(c.G) * float64(c.A) / 255
b += float64(c.B) * float64(c.A) / 255
a += float64(c.A)
}
}
if a > 0 {
img.SetNRGBA(x, y, color.NRGBA{uint8(r * 255 / a), uint8(g * 255 / a), uint8(b * 255 / a), uint8(a / (samples * samples))})
}
}
}
return img
}

func writeIcon(name string, warning bool) {
sizes := []int{16, 20, 24, 32, 40, 48, 64, 128, 256}
var frames [][]byte
for _, size := range sizes {
var frame bytes.Buffer
if err := png.Encode(&frame, glyph(size, warning)); err != nil {
panic(err)
}
frames = append(frames, frame.Bytes())
}
var output bytes.Buffer
write := func(value any) {
if err := binary.Write(&output, binary.LittleEndian, value); err != nil {
panic(err)
}
}
write(uint16(0))
write(uint16(1))
write(uint16(len(sizes)))
offset := 6 + 16*len(sizes)
for i, size := range sizes {
write([4]byte{byte(size % 256), byte(size % 256), 0, 0})
write(uint16(1))
write(uint16(32))
write(uint32(len(frames[i])))
write(uint32(offset))
offset += len(frames[i])
}
for _, frame := range frames {
output.Write(frame)
}
if err := os.WriteFile("assets/"+name+".ico", output.Bytes(), 0644); err != nil {
panic(err)
}
}

func main() { writeIcon("codexfold", false); writeIcon("codexfold-attention", true) }
7 changes: 7 additions & 0 deletions cmd/codexfold-tray/main_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !windows

package main

import "fmt"

func main() { fmt.Println("The CodexFold tray companion is available on Windows.") }
61 changes: 61 additions & 0 deletions cmd/codexfold-tray/main_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build windows

package main

//go:generate go run assets/generate.go
//go:generate go run github.com/akavel/rsrc@v0.10.2 -manifest assets/app.manifest -ico assets/codexfold.ico,assets/codexfold-attention.ico -arch amd64 -o rsrc_windows_amd64.syso
//go:generate go run github.com/akavel/rsrc@v0.10.2 -manifest assets/app.manifest -ico assets/codexfold.ico,assets/codexfold-attention.ico -arch arm64 -o rsrc_windows_arm64.syso

import (
"flag"
"fmt"
"os"
"path/filepath"
"time"

"github.com/samekind/codexfold/internal/codex"
"github.com/samekind/codexfold/internal/tray"
)

func main() {
store := flag.String("store", "", "CodexFold store directory; defaults to CODEX_HOME/fold-store")
background := flag.Bool("background", false, "Start in the notification area")
quit := flag.Bool("quit", false, "Close the tray for this store without stopping the filesystem")
diagnostics := flag.String("diagnostics", "", "Export aggregate diagnostics to a JSON file and exit")
flag.Parse()
if *store == "" {
home, err := codex.ResolveHome("")
if err != nil {
tray.ShowError(err)
os.Exit(1)
}
*store = filepath.Join(home, "fold-store")
}
absolute, err := filepath.Abs(*store)
if err == nil && *diagnostics != "" {
output, outputErr := filepath.Abs(*diagnostics)
if outputErr == nil {
monitor := tray.NewMonitor(absolute)
outputErr = tray.ExportDiagnostics(output, monitor.DiagnosticSnapshot(time.Now()))
}
if outputErr != nil {
fmt.Fprintln(os.Stderr, outputErr)
os.Exit(1)
}
return
}
if err == nil && *quit {
if err := tray.RequestExit(absolute); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
return
}
if err == nil {
err = tray.Run(absolute, *background)
}
if err != nil {
tray.ShowError(fmt.Errorf("CodexFold: %w", err))
os.Exit(1)
}
}
32 changes: 32 additions & 0 deletions cmd/codexfold-tray/resources_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//go:build windows

package main

import (
"golang.org/x/sys/windows"
"testing"
)

func TestWindowsIconResourcesAndDPIManifest(t *testing.T) {
var instance windows.Handle
if err := windows.GetModuleHandleEx(0, nil, &instance); err != nil {
t.Fatal(err)
}
user32 := windows.NewLazySystemDLL("user32.dll")
for _, resource := range []uintptr{2, 12} {
for _, size := range []uintptr{16, 24, 32, 48, 64} {
icon, _, err := user32.NewProc("LoadImageW").Call(uintptr(instance), resource, 1, size, size, 0)
if icon == 0 {
t.Fatalf("resource %d at %d pixels: %v", resource, size, err)
}
user32.NewProc("DestroyIcon").Call(icon)
}
}
if contextProc := user32.NewProc("GetThreadDpiAwarenessContext"); contextProc.Find() == nil {
context, _, _ := contextProc.Call()
equal, _, _ := user32.NewProc("AreDpiAwarenessContextsEqual").Call(context, ^uintptr(3))
if equal == 0 {
t.Fatal("tray executable does not enable PerMonitorV2 through its manifest")
}
}
}
Binary file added cmd/codexfold-tray/rsrc_windows_amd64.syso
Binary file not shown.
Binary file added cmd/codexfold-tray/rsrc_windows_arm64.syso
Binary file not shown.
Loading
Loading