Skip to main content

#!/usr/bin/env python3 """ macOS Code Signing & TCC Compliance Setup (ADR-190)

Interactive walkthrough that sets up:

  1. Self-signed code signing certificate (or detects existing)
  2. .app bundle wrapper for codi-watcher
  3. Code signing of the .app bundle
  4. Guides user through Full Disk Access grant
  5. Updates LaunchAgent to reference .app bundle

Usage: python3 scripts/setup-macos-code-signing.py # Full interactive setup python3 scripts/setup-macos-code-signing.py --check # Check current status python3 scripts/setup-macos-code-signing.py --resign # Re-sign after binary update python3 scripts/setup-macos-code-signing.py --non-interactive # Automated (no prompts)

For /sign-binary --setup-fda command integration.

Created: 2026-02-13 Track: H.0 (Framework Automation) ADR: ADR-190 (macOS Code Signing and TCC Compliance) """

import argparse import os import platform import re import shutil import subprocess import sys import tempfile from pathlib import Path from typing import Optional, Tuple

── Constants ──────────────────────────────────────────────────────────

HOME = Path.home() CERT_CN = "CODITECT Local Signing" CERT_ORG = "AZ1.AI INC" BUNDLE_ID = "ai.coditect.codi-watcher" BUNDLE_NAME = "CoDiWatcher" BINARY_NAME = "codi-watcher"

Platform-specific paths

if sys.platform == "darwin": PROTECTED_LOC = HOME / "Library" / "Application Support" / "CODITECT" / "core" else: print("This script is macOS-only. Linux has no TCC equivalent.") sys.exit(0)

BIN_DIR = PROTECTED_LOC / "bin" BINARY_PATH = BIN_DIR / BINARY_NAME APP_DIR = BIN_DIR / f"{BUNDLE_NAME}.app" APP_BINARY = APP_DIR / "Contents" / "MacOS" / BINARY_NAME LAUNCH_AGENT_PLIST = HOME / "Library" / "LaunchAgents" / "ai.coditect.context-watcher.plist" KEYCHAIN = HOME / "Library" / "Keychains" / "login.keychain-db"

── Colors ─────────────────────────────────────────────────────────────

class C: RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[93m' BLUE = '\033[94m' CYAN = '\033[96m' BOLD = '\033[1m' DIM = '\033[2m' END = '\033[0m'

def print_step(num: int, total: int, msg: str): print(f"\n{C.BOLD}{C.CYAN}[{num}/{total}]{C.END} {C.BOLD}{msg}{C.END}")

def print_ok(msg: str): print(f" {C.GREEN}✓{C.END} {msg}")

def print_warn(msg: str): print(f" {C.YELLOW}⚠{C.END} {msg}")

def print_err(msg: str): print(f" {C.RED}✗{C.END} {msg}")

def print_info(msg: str): print(f" {C.DIM}{msg}{C.END}")

def run(cmd, **kwargs) -> subprocess.CompletedProcess: """Run a command and return result.""" return subprocess.run(cmd, capture_output=True, text=True, **kwargs)

── Step 1: Check Prerequisites ───────────────────────────────────────

def check_prerequisites() -> bool: """Verify macOS, SIP status, and binary existence.""" if platform.system() != "Darwin": print_err("This script only runs on macOS") return False

if not BINARY_PATH.exists():
print_err(f"codi-watcher binary not found at: {BINARY_PATH}")
print_info("Install it first: python3 scripts/CODITECT-CORE-INITIAL-SETUP.py")
return False

# Check SIP status (informational)
result = run(["csrutil", "status"])
sip_enabled = "enabled" in result.stdout.lower()
if sip_enabled:
print_ok("SIP is enabled (expected — no TCC.db modification possible)")
else:
print_warn("SIP is disabled — TCC.db could be modified directly, but .app bundle approach is still recommended")

return True

── Step 2: Certificate Management ────────────────────────────────────

def find_signing_identity() -> Optional[str]: """Find existing CODITECT signing identity in keychain.""" result = run(["security", "find-identity", "-v", "-p", "codesigning"]) for line in result.stdout.split('\n'): if CERT_CN in line: match = re.search(r'"([^"]*)"', line) if match: return match.group(1) return None

def create_signing_certificate(non_interactive: bool = False) -> Optional[str]: """Create a self-signed code signing certificate.""" existing = find_signing_identity() if existing: print_ok(f"Signing identity already exists: "{existing}"") return existing

if not non_interactive:
print_info("No CODITECT signing certificate found in keychain.")
print_info("A self-signed certificate will be created for local code signing.")
response = input(f"\n Create \"{CERT_CN}\" certificate? [Y/n] ").strip().lower()
if response == 'n':
print_warn("Skipping certificate creation — .app bundle will use ad-hoc signing")
return None

print_info("Generating self-signed certificate...")

with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
key_file = tmp / "coditect-sign.key"
crt_file = tmp / "coditect-sign.crt"
p12_file = tmp / "coditect-sign.p12"

# Generate password
import secrets
password = secrets.token_hex(16)

# Generate certificate with code signing extensions
result = run([
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", str(key_file),
"-out", str(crt_file),
"-days", "3650", # 10 years
"-nodes",
"-subj", f"/CN={CERT_CN}/O={CERT_ORG}",
"-addext", "keyUsage=critical,digitalSignature",
"-addext", "extendedKeyUsage=critical,codeSigning",
"-addext", "basicConstraints=critical,CA:false",
])
if result.returncode != 0:
print_err(f"Certificate generation failed: {result.stderr}")
return None
print_ok("Certificate generated (RSA 2048-bit, 10-year validity)")

# Export to PKCS12 (MUST use -legacy for macOS Keychain compatibility)
result = run([
"openssl", "pkcs12", "-export", "-legacy",
"-out", str(p12_file),
"-inkey", str(key_file),
"-in", str(crt_file),
"-passout", f"pass:{password}",
])
if result.returncode != 0:
print_err(f"PKCS12 export failed: {result.stderr}")
return None
print_ok("PKCS12 exported with -legacy flag (macOS Keychain compatible)")

# Import into login keychain
result = run([
"security", "import", str(p12_file),
"-k", str(KEYCHAIN),
"-P", password,
"-T", "/usr/bin/codesign",
])
if result.returncode != 0:
print_err(f"Keychain import failed: {result.stderr}")
return None
print_ok("Imported to login keychain")

# Trust for code signing
result = run([
"security", "add-trusted-cert", "-p", "codeSign",
"-k", str(KEYCHAIN),
str(crt_file),
])
if result.returncode != 0:
print_err(f"Trust failed: {result.stderr}")
print_info("You may need to trust manually in Keychain Access")
return None
print_ok("Trusted for code signing")

# Verify identity is now available
identity = find_signing_identity()
if identity:
print_ok(f"Signing identity ready: \"{identity}\"")
return identity
else:
print_err("Certificate created but not found in codesigning identities")
print_info("Try: security find-identity -v -p codesigning")
return None

── Step 3: .app Bundle Creation ──────────────────────────────────────

def create_app_bundle() -> bool: """Create or update the .app bundle wrapper.""" contents_dir = APP_DIR / "Contents" macos_dir = contents_dir / "MacOS" plist_file = contents_dir / "Info.plist"

# Create directory structure
macos_dir.mkdir(parents=True, exist_ok=True)

# Get version from binary
version = "3.1.0"
try:
result = run([str(BINARY_PATH), "--version"])
if result.returncode == 0:
match = re.search(r'(\d+\.\d+\.\d+)', result.stdout)
if match:
version = match.group(1)
except Exception:
pass

# Write Info.plist
plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
CFBundleIdentifier {BUNDLE_ID} CFBundleName {BUNDLE_NAME} CFBundleDisplayName CODITECT Context Watcher CFBundleExecutable {BINARY_NAME} CFBundleVersion {version} CFBundleShortVersionString {version} CFBundlePackageType APPL LSBackgroundOnly LSUIElement """
plist_file.write_text(plist_content)
print_ok(f"Info.plist written (version {version})")

# Copy binary
shutil.copy2(str(BINARY_PATH), str(macos_dir / BINARY_NAME))
os.chmod(str(macos_dir / BINARY_NAME), 0o755)
print_ok(f"Binary copied into .app bundle")

return True

── Step 4: Code Signing ──────────────────────────────────────────────

def sign_app_bundle(identity: Optional[str]) -> bool: """Sign the .app bundle.""" if identity: result = run([ "codesign", "-fs", identity, "--identifier", BUNDLE_ID, str(APP_DIR), ]) if result.returncode == 0: print_ok(f"Signed with "{identity}"") else: print_err(f"Signing failed: {result.stderr}") return False else: # Ad-hoc signing (fallback) result = run([ "codesign", "-fs", "-", "--identifier", BUNDLE_ID, str(APP_DIR), ]) if result.returncode == 0: print_warn("Ad-hoc signed (no persistent identity — FDA may not stick across rebuilds)") else: print_err(f"Ad-hoc signing failed: {result.stderr}") return False

# Verify
result = run(["codesign", "-v", "--deep", str(APP_DIR)])
if result.returncode == 0:
print_ok("Signature verified")
else:
print_warn(f"Verification warning: {result.stderr}")

return True

── Step 5: LaunchAgent Update ────────────────────────────────────────

def update_launch_agent(non_interactive: bool = False) -> bool: """Update LaunchAgent to reference .app bundle binary.""" if not LAUNCH_AGENT_PLIST.exists(): print_warn("LaunchAgent plist not found — skipping update") print_info(f"Expected: {LAUNCH_AGENT_PLIST}") return True

content = LAUNCH_AGENT_PLIST.read_text()
expected_path = str(APP_BINARY)

if expected_path in content:
print_ok("LaunchAgent already references .app bundle binary")
return True

# Check if it references the old path
old_paths = [
str(BIN_DIR / BINARY_NAME),
f"{HOME}/.coditect/bin/{BINARY_NAME}",
]

updated = False
for old_path in old_paths:
if old_path in content:
if not non_interactive:
print_info(f"LaunchAgent currently references: {old_path}")
print_info(f"Will update to: {expected_path}")
response = input(f"\n Update LaunchAgent plist? [Y/n] ").strip().lower()
if response == 'n':
print_warn("Skipping LaunchAgent update")
return True

# Unload before modifying
run(["launchctl", "unload", str(LAUNCH_AGENT_PLIST)])

content = content.replace(old_path, expected_path)
LAUNCH_AGENT_PLIST.write_text(content)
print_ok("LaunchAgent plist updated")
updated = True
break

if not updated and expected_path not in content:
print_warn("LaunchAgent plist has unexpected ProgramArguments — manual update may be needed")
print_info(f"Binary should be: {expected_path}")
return True

return True

── Step 6: Full Disk Access Guidance ─────────────────────────────────

def guide_fda_grant(non_interactive: bool = False) -> bool: """Guide user through Full Disk Access grant.""" print_info("macOS requires a one-time manual Full Disk Access grant.") print_info("This is an Apple platform constraint — no programmatic alternative exists.") print() print(f" {C.BOLD}Steps:{C.END}") print(f" 1. System Settings will open to Privacy & Security > Full Disk Access") print(f" 2. Click the {C.BOLD}+{C.END} button") print(f" 3. Navigate to: {C.CYAN}{APP_DIR}{C.END}") print(f" (Press {C.BOLD}Cmd+Shift+G{C.END} to type the path, then paste)") print(f" 4. Select {C.BOLD}{BUNDLE_NAME}.app{C.END} and click Open") print(f" 5. Toggle it {C.GREEN}ON{C.END}") print()

if non_interactive:
# Open FDA pane automatically
subprocess.run(["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"])
print_ok("Opened System Settings FDA pane")
return True

response = input(f" Open System Settings now? [Y/n] ").strip().lower()
if response != 'n':
subprocess.run(["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"])
print_ok("Opened System Settings FDA pane")

# Copy path to clipboard for convenience
try:
subprocess.run(["pbcopy"], input=str(APP_DIR), text=True)
print_ok(f"Path copied to clipboard: {APP_DIR}")
except Exception:
pass

input(f"\n Press Enter after granting Full Disk Access... ")

return True

── Step 7: Reload LaunchAgent ────────────────────────────────────────

def reload_launch_agent() -> bool: """Reload the LaunchAgent.""" if not LAUNCH_AGENT_PLIST.exists(): print_warn("LaunchAgent plist not found — skipping reload") return True

# Unload (ignore errors if not loaded)
run(["launchctl", "unload", str(LAUNCH_AGENT_PLIST)])

# Load
result = run(["launchctl", "load", str(LAUNCH_AGENT_PLIST)])
if result.returncode == 0:
print_ok("LaunchAgent reloaded")
else:
print_warn(f"LaunchAgent load issue: {result.stderr}")
print_info(f"Manual: launchctl load {LAUNCH_AGENT_PLIST}")

return True

── Status Check ──────────────────────────────────────────────────────

def check_status(): """Check current code signing and TCC status.""" print(f"\n{C.BOLD}macOS Code Signing & TCC Status{C.END}") print("=" * 45)

# Binary
print(f"\n{C.BOLD}Binary:{C.END}")
if BINARY_PATH.exists():
size = BINARY_PATH.stat().st_size
print_ok(f"{BINARY_PATH} ({size:,} bytes)")
else:
print_err(f"Not found: {BINARY_PATH}")

# .app bundle
print(f"\n{C.BOLD}.app Bundle:{C.END}")
if APP_DIR.exists():
if APP_BINARY.exists():
print_ok(f"{APP_DIR}")
# Check signature
result = run(["codesign", "-dvv", str(APP_DIR)])
if result.returncode == 0:
for line in result.stderr.split('\n'):
if 'Authority=' in line or 'Identifier=' in line or 'TeamIdentifier=' in line:
print_info(f" {line.strip()}")
else:
print_warn("Not signed or signature invalid")
else:
print_warn(f".app exists but binary missing inside")
else:
print_err(f"Not found: {APP_DIR}")

# Signing identity
print(f"\n{C.BOLD}Signing Identity:{C.END}")
identity = find_signing_identity()
if identity:
print_ok(f"\"{identity}\"")
else:
print_warn("No CODITECT signing identity found")

# LaunchAgent
print(f"\n{C.BOLD}LaunchAgent:{C.END}")
if LAUNCH_AGENT_PLIST.exists():
content = LAUNCH_AGENT_PLIST.read_text()
if str(APP_BINARY) in content:
print_ok(f"References .app bundle binary")
elif str(BINARY_PATH) in content:
print_warn("References standalone binary (needs update for FDA)")
else:
print_info("References unknown path")

# Check if loaded
result = run(["launchctl", "list"])
if "ai.coditect.context-watcher" in result.stdout:
print_ok("Service is loaded")
else:
print_warn("Service is not loaded")
else:
print_err(f"Not found: {LAUNCH_AGENT_PLIST}")

# Full Disk Access (heuristic — check if watcher ran without errors recently)
print(f"\n{C.BOLD}Full Disk Access:{C.END}")
log_path = HOME / "PROJECTS" / ".coditect-data" / "logs" / "context-watcher.log"
if log_path.exists():
# Check last few lines for TCC errors
try:
content = log_path.read_text()
last_lines = content.strip().split('\n')[-20:]
tcc_errors = [l for l in last_lines if 'Operation not permitted' in l or 'kTCC' in l]
if tcc_errors:
print_warn("Recent TCC errors detected — FDA may not be granted")
for err in tcc_errors[-3:]:
print_info(f" {err[:100]}")
else:
print_ok("No recent TCC errors (FDA likely granted)")
except Exception:
print_info("Could not read log file")
else:
print_info("No watcher log found yet")

print()

── Re-sign Mode ──────────────────────────────────────────────────────

def resign_app_bundle(): """Quick re-sign after binary update.""" print(f"\n{C.BOLD}Re-signing CoDiWatcher.app{C.END}")

if not APP_DIR.exists():
print_err(".app bundle not found — run full setup first")
return False

if not BINARY_PATH.exists():
print_err("codi-watcher binary not found")
return False

# Copy fresh binary
shutil.copy2(str(BINARY_PATH), str(APP_BINARY))
os.chmod(str(APP_BINARY), 0o755)
print_ok("Binary updated")

# Sign
identity = find_signing_identity()
return sign_app_bundle(identity)

── Main ──────────────────────────────────────────────────────────────

def main(): parser = argparse.ArgumentParser( description="macOS Code Signing & TCC Compliance Setup (ADR-190)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 setup-macos-code-signing.py # Full interactive setup python3 setup-macos-code-signing.py --check # Status check python3 setup-macos-code-signing.py --resign # Re-sign after binary update python3 setup-macos-code-signing.py --non-interactive # Automated """ ) parser.add_argument("--check", action="store_true", help="Check current status without making changes") parser.add_argument("--resign", action="store_true", help="Re-copy and re-sign binary in .app bundle") parser.add_argument("--non-interactive", action="store_true", help="Run without prompts (for automation)")

args = parser.parse_args()

# Status check mode
if args.check:
check_status()
return

# Re-sign mode
if args.resign:
if resign_app_bundle():
print(f"\n{C.GREEN}{C.BOLD}Re-sign complete!{C.END}")
print_info("FDA grant persists (same bundle identifier)")
else:
print(f"\n{C.RED}Re-sign failed{C.END}")
sys.exit(1)
return

# Full interactive setup
total_steps = 7
print(f"\n{C.BOLD}{C.CYAN}CODITECT macOS Code Signing Setup (ADR-190){C.END}")
print("=" * 50)
print(f"{C.DIM}Sets up code signing + .app bundle + Full Disk Access{C.END}")
print(f"{C.DIM}for codi-watcher LaunchAgent TCC compliance.{C.END}")

# Step 1: Prerequisites
print_step(1, total_steps, "Checking prerequisites")
if not check_prerequisites():
sys.exit(1)

# Step 2: Certificate
print_step(2, total_steps, "Setting up signing certificate")
identity = create_signing_certificate(args.non_interactive)

# Step 3: .app bundle
print_step(3, total_steps, "Creating .app bundle")
if not create_app_bundle():
print_err("Failed to create .app bundle")
sys.exit(1)

# Step 4: Sign
print_step(4, total_steps, "Signing .app bundle")
if not sign_app_bundle(identity):
print_err("Failed to sign .app bundle")
sys.exit(1)

# Step 5: LaunchAgent
print_step(5, total_steps, "Updating LaunchAgent")
update_launch_agent(args.non_interactive)

# Step 6: FDA
print_step(6, total_steps, "Full Disk Access grant")
guide_fda_grant(args.non_interactive)

# Step 7: Reload
print_step(7, total_steps, "Reloading LaunchAgent")
reload_launch_agent()

# Summary
print(f"\n{C.GREEN}{C.BOLD}Setup complete!{C.END}")
print()
print(f" {C.BOLD}What was done:{C.END}")
if identity:
print(f" Certificate: \"{identity}\" in login keychain")
else:
print(f" Certificate: Ad-hoc (no persistent identity)")
print(f" .app bundle: {APP_DIR}")
print(f" Signed: {BUNDLE_ID}")
print(f" LaunchAgent: Updated to reference .app binary")
print()
print(f" {C.BOLD}After binary updates:{C.END}")
print(f" python3 scripts/setup-macos-code-signing.py --resign")
print(f" (or /sync handles this automatically)")
print()
print(f" {C.BOLD}Check status:{C.END}")
print(f" python3 scripts/setup-macos-code-signing.py --check")
print()

if name == "main": main()