feat(privacy-host): add signed native calendar contacts and reminders host
Add the owner-only AF_UNIX Reyna CLI privacy host, strict signed-app installation, and typed native routing for Calendar, Contacts, and Reminders.\n\nAdd bounded system-status paths and config-only direct local-service wrappers. Preserve MacMiniMCP pending explicit cutover approval.\n\nApple Notes is intentionally deferred: no native Notes operations, Apple Events declaration, or Automation helper are included; legacy Notes handling remains untouched.
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
"""Tests for Notes deferred status — no Notes integration may exist.
|
||||
|
||||
Assert:
|
||||
- no `notes.` protocol ops
|
||||
- no native_notes wrappers
|
||||
- no CLI Notes authorization/subcommands
|
||||
- AppleEvents usage key forbidden and absent from plist/app policy
|
||||
- no AppKit link/import
|
||||
- docs correctly say deferred + legacy untouched
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_no_notes_protocol_ops_in_privacy_contract():
|
||||
from reyna_cli.privacy_contract import ALLOWED_OPERATIONS, _COMMAND_TO_OPERATION
|
||||
|
||||
# No operation may start with notes.
|
||||
for op in ALLOWED_OPERATIONS.keys():
|
||||
assert not op.startswith("notes."), f"forbidden notes op {op} still present — Notes is deferred"
|
||||
|
||||
# Explicit strings must not appear
|
||||
forbidden_ops = {"notes.list", "notes.read", "notes.create", "notes.request_access"}
|
||||
for fo in forbidden_ops:
|
||||
assert fo not in ALLOWED_OPERATIONS, f"forbidden {fo} present"
|
||||
|
||||
for cmd, op in _COMMAND_TO_OPERATION.items():
|
||||
assert not op.startswith("notes."), f"command {cmd} maps to forbidden notes op {op}"
|
||||
assert "notes" not in cmd.lower() or "notes" in cmd.lower() and False is False # allow key detection via op only
|
||||
# also forbid notes_* commands mapping
|
||||
assert not cmd.startswith("notes_"), f"forbidden notes command {cmd}"
|
||||
|
||||
|
||||
def test_no_native_notes_wrappers():
|
||||
src = (REPO / "src" / "reyna_cli" / "privacy_host.py").read_text()
|
||||
assert "native_notes" not in src, "native_notes wrappers must be removed — Notes deferred"
|
||||
assert "NotesProvider" not in src
|
||||
assert "NotesAuthorization" not in src
|
||||
# generic notes field as parameter name for calendar/reminders is allowed,
|
||||
# but operation names notes.list etc are forbidden — already checked by string search above
|
||||
# Ensure no wrapper defs remain
|
||||
for name in ["native_notes_list", "native_notes_read", "native_notes_create", "native_notes_request_access"]:
|
||||
assert name not in src
|
||||
|
||||
|
||||
def test_no_cli_notes_subcommands():
|
||||
cli_src = (REPO / "src" / "reyna_cli" / "cli.py").read_text()
|
||||
# macmini notes subcommands must be gone
|
||||
assert "macmini_notes" not in cli_src, "macmini_notes commands must be removed"
|
||||
# CLI must not register a notes typer under macmini
|
||||
# Check that macmini help mentions notes is gone — we test via source: no notes app registration
|
||||
# Allow word 'notes' as parameter name (calendar notes, reminder notes) — but not as subcommand registration
|
||||
# So forbid 'notes' app creation for macmini
|
||||
# Look for macmini_notes typed list/read/create defs
|
||||
assert "def macmini_notes" not in cli_src
|
||||
# Privacy-host notes-authorize must be gone
|
||||
assert "notes-authorize" not in cli_src
|
||||
assert "notes_authorize" not in cli_src
|
||||
# native_notes usage in cli must be gone
|
||||
assert "native_notes" not in cli_src
|
||||
# Ensure NotesProvider strings absent
|
||||
assert "NotesProvider" not in cli_src
|
||||
|
||||
|
||||
def test_appleevents_usage_forbidden():
|
||||
from reyna_cli import app_bundle as ab
|
||||
|
||||
d = ab.build_app_bundle_info_plist_dict()
|
||||
assert "NSAppleEventsUsageDescription" not in d, "AppleEvents usage must be forbidden — Notes deferred"
|
||||
# Also check Xcode source plist
|
||||
xcode_plist = REPO / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "Info.plist"
|
||||
assert xcode_plist.exists()
|
||||
with open(xcode_plist, "rb") as f:
|
||||
xd = plistlib.load(f)
|
||||
assert "NSAppleEventsUsageDescription" not in xd, "Xcode Info.plist must not contain AppleEvents"
|
||||
|
||||
# App bundle policy: forbidden key must be flagged
|
||||
# Validate that forbidden set includes only Calendar, Contacts, Reminders
|
||||
allowed = {
|
||||
"NSCalendarsFullAccessUsageDescription",
|
||||
"NSCalendarsWriteOnlyAccessUsageDescription",
|
||||
"NSCalendarsUsageDescription",
|
||||
"NSContactsUsageDescription",
|
||||
"NSRemindersFullAccessUsageDescription",
|
||||
}
|
||||
for k in d.keys():
|
||||
if k.startswith("NS") and "UsageDescription" in k:
|
||||
assert k in allowed, f"unexpected usage key {k} — only calendar/contacts/reminders allowed (Notes deferred)"
|
||||
|
||||
|
||||
def test_app_bundle_validator_forbids_appleevents():
|
||||
from reyna_cli import app_bundle as ab
|
||||
import tempfile
|
||||
|
||||
repo_root = Path(tempfile.mkdtemp()) / "repo"
|
||||
bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app"
|
||||
contents = bundle / "Contents"
|
||||
macos = contents / "MacOS"
|
||||
macos.mkdir(parents=True)
|
||||
exe = macos / "ReynaCLIHost"
|
||||
exe.write_bytes(b"binary")
|
||||
exe.chmod(0o755)
|
||||
|
||||
class Proc:
|
||||
returncode = 0
|
||||
stdout = ""
|
||||
stderr = "TeamIdentifier=TEAM123\nAuthority=Apple Development: Foo (TEAM123)\n"
|
||||
|
||||
def runner_ok(args, cwd=None, **kwargs):
|
||||
return Proc()
|
||||
|
||||
good = ab.build_app_bundle_info_plist_dict()
|
||||
with open(contents / "Info.plist", "wb") as f:
|
||||
plistlib.dump(good, f)
|
||||
res_ok = ab.validate_app_bundle(repo_root=repo_root, runner=runner_ok)
|
||||
assert res_ok["ok"] is True, f"should accept plist without AppleEvents: {res_ok['errors']}"
|
||||
|
||||
bad = dict(good)
|
||||
bad["NSAppleEventsUsageDescription"] = "Allow automation"
|
||||
with open(contents / "Info.plist", "wb") as f:
|
||||
plistlib.dump(bad, f)
|
||||
res_bad = ab.validate_app_bundle(repo_root=repo_root, runner=runner_ok)
|
||||
assert res_bad["ok"] is False
|
||||
assert any("appleevents" in e.lower() or "unexpected" in e.lower() or "forbidden" in e.lower() for e in res_bad["errors"])
|
||||
|
||||
|
||||
def test_no_appkit_link_or_import():
|
||||
# AppMain and AppEntry must not import AppKit, must not contain AppleEvents
|
||||
app_main = (REPO / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "AppMain.swift").read_text()
|
||||
app_entry = (REPO / "native" / "ReynaCLIHost" / "Sources" / "ReynaCLIHostCore" / "AppEntry.swift").read_text()
|
||||
for txt, label in [(app_main, "AppMain"), (app_entry, "AppEntry")]:
|
||||
assert "AppKit" not in txt, f"{label} must not import AppKit — Notes deferred, headless"
|
||||
assert "NSAppleScript" not in txt
|
||||
assert "NSAppleEvent" not in txt
|
||||
assert "AppleEvents" not in txt
|
||||
|
||||
# Package.swift must not link AppKit
|
||||
pkg = (REPO / "native" / "ReynaCLIHost" / "Package.swift").read_text()
|
||||
assert "AppKit" not in pkg
|
||||
# pbxproj must not contain AppKit, NotesProvider, NotesAuthorization, ForegroundApp, AppleEventsUsageDescription
|
||||
pbx = (REPO / "native" / "ReynaCLIHost" / "ReynaCLIHost.xcodeproj" / "project.pbxproj").read_text()
|
||||
assert "AppKit.framework" not in pbx
|
||||
assert "NotesProvider.swift" not in pbx, "NotesProvider must not be in Xcode project — deferred"
|
||||
assert "NotesAuthorizationProvider.swift" not in pbx
|
||||
assert "ForegroundApp" not in pbx
|
||||
assert "ForegroundAuthorization" not in pbx
|
||||
assert "NSAppleEventsUsageDescription" not in pbx
|
||||
|
||||
# Protocol.swift must not reference notes operations
|
||||
proto = (REPO / "native" / "ReynaCLIHost" / "Sources" / "ReynaCLIHostCore" / "Protocol.swift").read_text()
|
||||
assert "notes.list" not in proto.lower()
|
||||
assert "notes.read" not in proto.lower()
|
||||
assert "notes.create" not in proto.lower()
|
||||
assert "notes.request_access" not in proto.lower()
|
||||
# Generic notes text field for calendar/reminders is allowed, but operation "notes." must not exist
|
||||
# Check for NotesProvider types
|
||||
assert "NotesProvider" not in proto
|
||||
assert "NotesAuthorization" not in proto
|
||||
assert "NoteListItem" not in proto and "NoteDetailItem" not in proto, "Notes data models must be removed"
|
||||
|
||||
|
||||
def test_no_notes_in_swift_sources():
|
||||
core_dir = REPO / "native" / "ReynaCLIHost" / "Sources" / "ReynaCLIHostCore"
|
||||
for p in core_dir.glob("*.swift"):
|
||||
txt = p.read_text()
|
||||
low = txt.lower()
|
||||
# forbid notes. ops
|
||||
assert "notes.list" not in low
|
||||
assert "notes.read" not in low
|
||||
assert "notes.create" not in low
|
||||
assert "notes.request_access" not in low
|
||||
# forbid NSAppleEvents and Notes-type providers
|
||||
assert "nsappleeventsusagedescription" not in low
|
||||
assert "NotesProvider" not in txt
|
||||
assert "NotesAuthorization" not in txt
|
||||
|
||||
|
||||
def test_swift_tests_deleted():
|
||||
tests_dir = REPO / "native" / "ReynaCLIHost" / "Tests" / "ReynaCLIHostTests"
|
||||
forbidden = ["NotesAuthorizationTests.swift", "NotesOperationsTests.swift", "ForegroundAuthorizationTests.swift"]
|
||||
for name in forbidden:
|
||||
assert not (tests_dir / name).exists(), f"{name} must be deleted"
|
||||
|
||||
|
||||
def test_python_notes_tests_deleted():
|
||||
assert not (REPO / "tests" / "test_foreground_notes_ls_authorize.py").exists()
|
||||
assert not (REPO / "tests" / "test_notes_authorization_boundary.py").exists()
|
||||
|
||||
|
||||
def test_docs_deferred_legacy_untouched():
|
||||
matrix_path = REPO / "docs" / "remaining-coverage-matrix.md"
|
||||
assert matrix_path.exists()
|
||||
txt = matrix_path.read_text()
|
||||
low = txt.lower()
|
||||
# Must say Notes deferred
|
||||
assert "notes" in low
|
||||
assert "deferred" in low, "docs must say Notes deferred"
|
||||
# Must say legacy untouched
|
||||
assert "legacy" in low
|
||||
assert "untouched" in low, "docs must say legacy untouched"
|
||||
# Must not say Notes belongs inside host as active work — should be in deferred section
|
||||
# Ensure no active plan to add NotesProvider now
|
||||
# Allow legacy mention but not as A TODO — check that if Notes is mentioned as A belongs, it's qualified as deferred
|
||||
# Simplest: ensure the doc contains explicit deferred banner
|
||||
assert "deferred" in txt, "doc must contain deferred word"
|
||||
# Ensure no forbidden old instructions about adding NotesProvider as immediate work without deferred qualifier
|
||||
# The deferred table should list Notes as deferred, not as Done A
|
||||
# We'll just ensure the word deferred appears near Notes line
|
||||
for line in txt.splitlines():
|
||||
if "notes" in line.lower() and ("list/read/create" in line.lower() or "notes.js" in line.lower()):
|
||||
# in that row, must mention deferred or C or out of scope
|
||||
assert "deferred" in line.lower() or "untouched" in txt.lower(), f"Notes row must mention deferred: {line}"
|
||||
|
||||
|
||||
def test_no_foreground_notes_references():
|
||||
# Search all source/test tree for forbidden patterns — but allow generic param named notes (calendar/reminder text)
|
||||
forbidden_exact = [
|
||||
"NotesProvider",
|
||||
"NotesAuthorization",
|
||||
"ForegroundApp",
|
||||
"foreground-notes",
|
||||
"NSAppleEventsUsageDescription",
|
||||
"notes.request_access",
|
||||
"native_notes",
|
||||
]
|
||||
exclude_dirs = {".venv", "__pycache__", ".git", "build", ".build", "DerivedData", "dist"}
|
||||
for pattern in forbidden_exact:
|
||||
for path in REPO.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
# skip excluded
|
||||
if any(part in exclude_dirs for part in path.parts):
|
||||
continue
|
||||
# skip backup
|
||||
if "backup" in path.parts:
|
||||
continue
|
||||
# Only check relevant extensions
|
||||
if path.suffix not in {".py", ".swift", ".plist", ".md", ".pbxproj", ".toml", ".yaml", ".yml"}:
|
||||
# also check .xcodeproj is dir, pbxproj covered
|
||||
if path.name != "project.pbxproj":
|
||||
continue
|
||||
# Skip this test file itself if pattern is mentioned in test strings — we need to allow self-reference check for patterns inside this file?
|
||||
# For this file we will skip self to avoid false positive on literal search
|
||||
if path.name == "test_notes_deferred.py" or "tests" in path.parts:
|
||||
continue
|
||||
# Skip docs remaining-coverage — allowed to mention pattern but must also say deferred; we already validated
|
||||
# However per task: eliminate only actual Notes integration references (do not remove generic calendar/reminder text fields named notes)
|
||||
# For forbidden patterns search, we strictly forbid integration references in source/test, not docs describing deferred
|
||||
if "docs/" in str(path) and pattern == "NSAppleEventsUsageDescription":
|
||||
continue
|
||||
if path.name == "app_bundle.py" and pattern == "NSAppleEventsUsageDescription":
|
||||
continue
|
||||
try:
|
||||
txt = path.read_text(errors="ignore")
|
||||
except Exception:
|
||||
continue
|
||||
if pattern in txt:
|
||||
# Allow generic reminder/calendar 'notes' param already excluded by exact list above — so any hit is real integration
|
||||
# But also allow mention in backup
|
||||
if "test_notes_deferred" in str(path):
|
||||
continue
|
||||
raise AssertionError(f"forbidden pattern '{pattern}' found in {path}")
|
||||
Reference in New Issue
Block a user