#679 spawn: is_owner() case-folds — is_owner('DEVPULSE') == is_owner('devpulse').

registry.is_owner (apps/handlers/registry.py:382) @-normalized the email but never lowercased, so a mixed-case branch name (registry names are mixed-case: DEVPULSE vs devpulse) returned False against the seated owner while the lowercase form returned True. Harmless today — the only live caller (@ai_mail dispatch_monitor._wake_sender) lowercases first — but the frozen TDPLAN-0012 contract promises a normalized email, and PART-4 owner-gating of watchdog/feedback may pass a raw branch name.

Fix: lowercase BOTH sides of the comparison (passed-in email AND registry owner email), @-strip preserved. +1 case-insensitivity test. Built by @spawn, verified by devpulse: LIVE repro — every case variant of the owner (DEVPULSE/@DEVPULSE/DevPulse) resolves True, non-owners (seedgo/@SEEDGO) and empty stay False; 316 spawn tests green (+1), seedgo 100%.

Rides PR#659 (issue-clearing, no main-merge). Source: #678/TDPLAN-0012 verify.
This commit is contained in:
AIOSAI
2026-07-10 04:06:19 -07:00
parent c970c5761f
commit a3a476f59d
3 changed files with 20 additions and 2 deletions
+9
View File
@@ -47,6 +47,15 @@ PyPI version — not the changelog header.
relays. Stall logic extracted into a `StallTracker` for clarity; +9 tests
(142 green), devpulse audit 100%. (devpulse)
- **`is_owner()` now case-folds — `is_owner('DEVPULSE')` matches `is_owner('devpulse')`
(issue #679).** The spawn-registry resolver (`registry.py:382`) `@`-normalized the
email but never lowercased, so a mixed-case branch name (registry names are
mixed-case: `DEVPULSE` vs `devpulse`) returned `False` against the seated owner.
Harmless today (the only caller lowercases first) but the frozen TDPLAN-0012
contract promises normalization, and PART-4 owner-gating may pass a raw name.
Now lowercases both sides; verified live (every case variant of the owner → True,
non-owners → False) + a case-insensitivity test (316 green). (@spawn, verified devpulse)
- **`aipass install` no longer hard-fails (exit 2, silently) when it can't create
global symlinks (issue #660 follow-up).** `setup.sh` runs under
`set -euo pipefail`; the #660 `safe_symlink` refactor returns `2` on `ln`
+2 -2
View File
@@ -379,10 +379,10 @@ def is_owner(email, start_path=None):
"""
if not email:
return False
normalized = email if email.startswith("@") else f"@{email}"
normalized = (email if email.startswith("@") else f"@{email}").lower()
owner = get_owner(start_path=start_path)
if owner is None:
return False
owner_email = owner.get("email", "")
owner_normalized = owner_email if owner_email.startswith("@") else f"@{owner_email}"
owner_normalized = (owner_email if owner_email.startswith("@") else f"@{owner_email}").lower()
return normalized == owner_normalized
@@ -154,6 +154,15 @@ class TestIsOwner:
with patch("aipass.spawn.apps.handlers.registry.find_registry", return_value=registry_with_owner):
assert is_owner("devpulse", start_path=tmp_path) is True
def test_case_insensitive(self, registry_with_owner, tmp_path):
from aipass.spawn.apps.handlers.registry import is_owner
with patch("aipass.spawn.apps.handlers.registry.find_registry", return_value=registry_with_owner):
assert is_owner("DEVPULSE", start_path=tmp_path) is True
assert is_owner("@DEVPULSE", start_path=tmp_path) is True
assert is_owner("DevPulse", start_path=tmp_path) is True
assert is_owner("ALPHA", start_path=tmp_path) is False
def test_false_for_non_owner(self, registry_with_owner, tmp_path):
from aipass.spawn.apps.handlers.registry import is_owner