feat(system): fix(windows): handler guard backslash path fix — all 11 __init__.py files (#304). caller_file.replace('\\', '/') normalizes Windows paths before the same-branch check. This is the actual fix for #293 which was incorrectly closed. drone is completely broken on Windows without this. (#306)

* feat(system): ci: add Windows setup test workflow — runs setup.sh + drone CLI verification on windows-latest GitHub Actions runner. Triggers on changes to setup.sh, handler __init__.py files, cli.py, or pyproject.toml. Closes the 'we never tested on Windows' gap.

Co-Authored-By: @devpulse <devpulse@aipass>

* feat(system): fix(windows): SIGPIPE guard in flow.py (#301) + fcntl platform guards in trigger/config.py and watchdog/registry.py (#302) — lazy import fcntl on Unix only, no-op on Windows. inbox_lock.py already cross-platform (msvcrt). ai_mail.py already guarded (hasattr check).

Co-Authored-By: @devpulse <devpulse@aipass>

* feat(system): fix(windows): handler guard backslash path fix — all 11 __init__.py files (#304). caller_file.replace('\\', '/') normalizes Windows paths before the same-branch check. This is the actual fix for #293 which was incorrectly closed. drone is completely broken on Windows without this.

Co-Authored-By: @devpulse <devpulse@aipass>

---------

Co-authored-by: @devpulse <devpulse@aipass>
This commit is contained in:
AIPass
2026-04-16 00:06:02 -07:00
committed by GitHub
co-authored by @devpulse
parent d0391f01d6
commit 504df48e82
15 changed files with 69 additions and 21 deletions
+38
View File
@@ -0,0 +1,38 @@
name: Windows Setup Test
on:
push:
branches: [main]
paths:
- 'setup.sh'
- 'src/aipass/*/apps/handlers/__init__.py'
- 'src/aipass/drone/cli.py'
- 'pyproject.toml'
pull_request:
paths:
- 'setup.sh'
- 'src/aipass/*/apps/handlers/__init__.py'
- 'src/aipass/drone/cli.py'
- 'pyproject.toml'
jobs:
windows-setup:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Run setup.sh
shell: bash
run: bash setup.sh
- name: Verify drone CLI
shell: bash
run: |
export PYTHONUTF8=1
drone --version
drone systems
drone @seedgo --help
+1 -1
View File
@@ -78,7 +78,7 @@ def _guard_branch_access():
return # Allow if truly can't determine
# Check if caller is from our branch
if f"/{MY_BRANCH}/" in caller_file:
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
return # Same branch, allowed
# External caller - block access
+1 -1
View File
@@ -53,7 +53,7 @@ def _guard_branch_access():
return # Allow command-line Python through
return
if f"/{MY_BRANCH}/" in caller_file:
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
return
caller_branch = _extract_branch_name(caller_file)
+1 -1
View File
@@ -99,7 +99,7 @@ def _guard_branch_access():
return # Allow if truly can't determine
# Check if caller is from our branch
if f"/{MY_BRANCH}/" in caller_file:
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
return # Same branch, allowed
# External caller - block access
@@ -40,7 +40,6 @@ Registry file schema (version 1):
}
"""
import fcntl
import json
import os
import secrets
@@ -122,7 +121,7 @@ def _atomic_write_unlocked(storage_path: Path, data: dict) -> None:
class _FileLock:
"""fcntl.flock-based exclusive lock on a sibling .lock file.
"""Exclusive lock on a sibling .lock file (fcntl on Unix, no-op on Windows).
Using a sibling avoids racing with the atomic replace of the data file:
if we locked the data file itself, os.replace would swap the inode out
@@ -134,6 +133,9 @@ class _FileLock:
self._fh = None
def __enter__(self) -> "_FileLock":
if sys.platform == "win32":
return self # Windows: skip file locking (single-user typical)
import fcntl
self._lock_path.parent.mkdir(parents=True, exist_ok=True)
# 'a+' so the file is created if missing and lock survives concurrent opens.
self._fh = open(self._lock_path, "a+", encoding='utf-8')
@@ -143,6 +145,7 @@ class _FileLock:
def __exit__(self, exc_type, exc, tb) -> None:
if self._fh is not None:
try:
import fcntl
fcntl.flock(self._fh.fileno(), fcntl.LOCK_UN)
finally:
self._fh.close()
+1 -1
View File
@@ -80,7 +80,7 @@ def _guard_branch_access():
# Check if caller is from our branch
# MY_BRANCH is "aipass.drone" (dotted), but filesystem uses "/aipass/drone/"
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
if branch_path in caller_file:
if branch_path in caller_file.replace("\\", "/"):
return # Same branch, allowed
# External caller - block access
+3 -1
View File
@@ -26,7 +26,9 @@ import signal
from typing import List, Any
# Handle broken pipe gracefully (e.g. output piped to head)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
# SIGPIPE does not exist on Windows
if hasattr(signal, 'SIGPIPE'):
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
# Prax logger
from aipass.prax.apps.modules.logger import system_logger as logger
+1 -1
View File
@@ -78,7 +78,7 @@ def _guard_branch_access():
return # Allow if truly can't determine
# Check if caller is from our branch
if f"/{MY_BRANCH}/" in caller_file:
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
return # Same branch, allowed
# External caller - block access
+1 -1
View File
@@ -80,7 +80,7 @@ def _guard_branch_access():
# Check if caller is from our branch
# MY_BRANCH is "aipass.memory" (dotted), but filesystem uses "/aipass/memory/"
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
if branch_path in caller_file:
if branch_path in caller_file.replace("\\", "/"):
return # Same branch, allowed
# External caller - block access
+1 -1
View File
@@ -80,7 +80,7 @@ def _guard_branch_access():
# Check if caller is from our branch
# MY_BRANCH is "aipass.prax" (dotted), but filesystem uses "/aipass/prax/"
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
if branch_path in caller_file:
if branch_path in caller_file.replace("\\", "/"):
return # Same branch, allowed
# External caller - block access
+1 -1
View File
@@ -85,7 +85,7 @@ def _guard_branch_access():
# Check if caller is from our branch
# MY_BRANCH is "aipass.seedgo" (dotted), but filesystem uses "/aipass/seedgo/"
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
if branch_path in caller_file:
if branch_path in caller_file.replace("\\", "/"):
return # Same branch, allowed
# External caller - block access
+1 -1
View File
@@ -59,7 +59,7 @@ def _guard_branch_access():
return
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
if branch_path in caller_file:
if branch_path in caller_file.replace("\\", "/"):
return
caller_branch = _extract_branch_name(caller_file)
@@ -59,7 +59,7 @@ def _guard_branch_access():
return
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
if branch_path in caller_file:
if branch_path in caller_file.replace("\\", "/"):
return
caller_branch = _extract_branch_name(caller_file)
+12 -7
View File
@@ -13,8 +13,8 @@ Provides package-relative paths for trigger data directories.
Works in both pip-installed and development environments.
"""
import fcntl
import json
import sys
import os
import tempfile
from contextlib import contextmanager
@@ -81,12 +81,17 @@ def json_file_lock(path: Path):
"""
lock_path = path.with_suffix('.lock')
lock_path.parent.mkdir(parents=True, exist_ok=True)
with open(lock_path, 'w', encoding='utf-8') as lock_f:
fcntl.flock(lock_f, fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(lock_f, fcntl.LOCK_UN)
if sys.platform == "win32":
# Windows: no fcntl, skip file locking (single-user typical)
yield
else:
import fcntl
with open(lock_path, 'w', encoding='utf-8') as lock_f:
fcntl.flock(lock_f, fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(lock_f, fcntl.LOCK_UN)
def print_introspection():
+1 -1
View File
@@ -53,7 +53,7 @@ def _guard_branch_access():
return # Allow command-line Python through
return
if f"/trigger/" in caller_file:
if f"/trigger/" in caller_file.replace("\\", "/"):
return
caller_branch = _extract_branch_name(caller_file)