mcpField
M
macos-accessibility

macos-accessibility

github.com/martinholovsky開発48311 回インストール47 週間前に更新
リポジトリ

macos-accessibility について

macos-accessibilityは、権限やセキュリティ境界、信頼性を考慮したmacOSアクセシビリティ自動化の設計スキルです。APIの実装パターン、テスト先行の手順、アプリのUIを操作する際の保護策を示します。

主な機能

  1. 01AXUIElementとAXObserverの指針
  2. 02TCC権限の検証
  3. 03プロセスとコード署名の確認
  4. 04アプリのブロックリストとタイムアウト
  5. 05キャッシュと属性の一括クエリ
  6. 06テスト先行の実装手順

活用シーン

  1. 01UI自動化の前に権限確認を設計
  2. 02アプリのアクセシビリティ要素を安全に調査
  3. 03自動化に監査ログとタイムアウトを追加
  4. 04アクセシビリティクエリの応答性を改善

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/martinholovsky/claude-skills-generator --skill macos-accessibility

インストール後、Claude Code で/macos-accessibilityを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
advanced-patterns.md
security-examples.md
threat-model.md

詳細

1. 概要

リスクレベル:高 — システムレベルのアクセス、TCC 権限の要件、プロセスとの対話

macOS のアクセシビリティ自動化に精通したエキスパートであり、以下の分野に深い専門知識を持っています。

  • AXUIElement API:アクセシビリティ要素の階層、属性、アクション
  • TCC(Transparency, Consent, Control):権限管理
  • ApplicationServices フレームワーク:システムレベルの自動化との統合
  • セキュリティ境界:サンドボックスの制限、ハードニングされたランタイム
主な専門分野
  1. アクセシビリティ API:AXUIElementRef、AXObserver、属性クエリ
  2. TCC 権限:アクセシビリティ権限のリクエスト、検証
  3. プロセス管理:NSRunningApplication、プロセスの検証
  4. セキュリティ制御:サンドボックスへの対応、権限レベル

2. 主な責務

2.1 基本原則
  • まず TDD:実装前にテストを作成し、権限チェック、要素クエリ、アクションが正しく動作することを確認します
  • パフォーマンスを意識:要素をキャッシュし、検索範囲を限定し、属性クエリをまとめて、応答性を最適化します
  • セキュリティを最優先:TCC 権限を検証し、コード署名を確認し、機密性の高いアプリケーションをブロックします
  • すべてを監査:セキュリティ監査証跡のため、相関 ID を付けてすべての操作をログに記録します
2.2 安全な自動化の原則

アクセシビリティ自動化を実行する際は、以下を守ります。

  • 操作の前に TCC 権限を検証 します
  • 対象アプリケーションの サンドボックス境界を尊重 します
  • 機密性の高いアプリケーション(Keychain、セキュリティ設定)をブロックします
  • 監査証跡のため、すべての操作をログに記録 します
  • ハングを防ぐため、タイムアウトを実装 します
2.3 権限管理

すべての自動化では、以下を行う必要があります。

  1. TCC データベースでアクセシビリティ権限を確認する
  2. プロセスが必要なエンタイトルメントを持っていることを検証する
  3. 必要最小限の権限をリクエストする
  4. 権限が拒否された場合に適切に処理する
2.4 セキュリティ最優先のアプローチ

自動化の各操作では、必ず以下を行います。

  1. 対象アプリケーションの ID を確認する
  2. ブロック対象のアプリケーションリストと照合する
  3. TCC 権限を検証する
  4. 相関 ID を付けて操作をログに記録する
  5. タイムアウト制限を適用する

3. 技術基盤

3.1 主なフレームワーク

主なフレームワーク:ApplicationServices / HIServices

  • 主要 API:AXUIElementRef(CFType ベースのアクセシビリティ要素)
  • オブザーバー API:イベント監視用の AXObserver
  • 属性 API:AXUIElementCopyAttributeValue

主な依存関係:

ApplicationServices.framework  # Core accessibility APIs
CoreFoundation.framework       # CFType support
AppKit.framework              # NSRunningApplication
Security.framework            # TCC queries
3.2 必須ライブラリ
ライブラリ用途セキュリティに関する注意事項
pyobjc-framework-ApplicationServicesPython バインディング要素へのアクセスを検証します
atomac高水準ラッパー使用前に TCC を確認します
pyautogui入力のシミュレーションアクセシビリティ権限が必要です

4. 実装パターン

パターン 1:TCC 権限の検証
import subprocess
from ApplicationServices import (
    AXIsProcessTrustedWithOptions,
    kAXTrustedCheckOptionPrompt
)

class TCCValidator:
    """Validate TCC permissions before automation."""

    @staticmethod
    def check_accessibility_permission(prompt: bool = False) -> bool:
        """Check if process has accessibility permission."""
        options = {kAXTrustedCheckOptionPrompt: prompt}
        return AXIsProcessTrustedWithOptions(options)

    @staticmethod
    def get_tcc_status(bundle_id: str) -> str:
        """Query TCC database for permission status."""
        query = f"""
        SELECT client, auth_value FROM access
        WHERE service = 'kTCCServiceAccessibility'
        AND client = '{bundle_id}'
        """
        # Note: Direct TCC database access requires SIP disabled
        # Use AXIsProcessTrusted for normal operation
        pass

    def ensure_permission(self):
        """Ensure accessibility permission is granted."""
        if not self.check_accessibility_permission():
            raise PermissionError(
                "Accessibility permission required. "
                "Enable in System Preferences > Security & Privacy > Accessibility"
            )
パターン 2:安全な要素の検出
from ApplicationServices import (
    AXUIElementCreateSystemWide,
    AXUIElementCreateApplication,
    AXUIElementCopyAttributeValue,
    AXUIElementCopyAttributeNames,
)
from Quartz import kAXErrorSuccess
import logging

class SecureAXAutomation:
    """Secure wrapper for AXUIElement automation."""

    BLOCKED_APPS = {
        'com.apple.keychainaccess',           # Keychain Access
        'com.apple.systempreferences',         # System Preferences
        'com.apple.SecurityAgent',             # Security dialogs
        'com.apple.Terminal',                  # Terminal
        'com.1password.1password',             # 1Password
    }

    def __init__(self, permission_tier: str = 'read-only'):
        self.permission_tier = permission_tier
        self.logger = logging.getLogger('ax.security')
        self.operation_timeout = 30

        # Validate TCC permission on init
        if not TCCValidator.check_accessibility_permission():
            raise PermissionError("Accessibility permission required")

    def get_application_element(self, pid: int) -> 'AXUIElementRef':
        """Get application element with validation."""
        # Get bundle ID
        bundle_id = self._get_bundle_id(pid)

        # Security check
        if bundle_id in self.BLOCKED_APPS:
            self.logger.warning(
                'blocked_app_access',
                bundle_id=bundle_id,
                reason='security_policy'
            )
            raise SecurityError(f"Access to {bundle_id} is blocked")

        # Create element
        app_element = AXUIElementCreateApplication(pid)

        self._audit_log('app_element_created', bundle_id, pid)
        return app_element

    def get_attribute(self, element, attribute: str):
        """Get element attribute with security filtering."""
        sensitive = ['AXValue', 'AXSelectedText', 'AXDocument']
        if attribute in sensitive and self.permission_tier == 'read-only':
            raise SecurityError(f"Access to {attribute} requires elevated permissions")

        error, value = AXUIElementCopyAttributeValue(element, attribute, None)
        if error != kAXErrorSuccess:
            return None

        # Redact password values
        return '[REDACTED]' if 'password' in str(attribute).lower() else value

    def _audit_log(self, action: str, bundle_id: str, pid: int):
        self.logger.info(f'ax.{action}', extra={
            'bundle_id': bundle_id, 'pid': pid, 'permission_tier': self.permission_tier
        })
パターン 3:安全なアクションの実行
from ApplicationServices import AXUIElementPerformAction

class SafeActionExecutor:
    """Execute AX actions with security controls."""
    BLOCKED_ACTIONS = {
        'read-only': ['AXPress', 'AXIncrement', 'AXDecrement', 'AXConfirm'],
        'standard': ['AXDelete', 'AXCancel'],
    }

    def __init__(self, permission_tier: str):
        self.permission_tier = permission_tier

    def perform_action(self, element, action: str):
        blocked = self.BLOCKED_ACTIONS.get(self.permission_tier, [])
        if action in blocked:
            raise PermissionError(f"Action {action} not allowed in {self.permission_tier} tier")
        error = AXUIElementPerformAction(element, action)
        return error == kAXErrorSuccess
パターン 4:アプリケーションの監視
from AppKit import NSWorkspace, NSRunningApplication

class ApplicationMonitor:
    """Monitor and validate running applications."""

    def get_frontmost_app(self) -> dict:
        app = NSWorkspace.sharedWorkspace().frontmostApplication()
        return {
            'pid': app.processIdentifier(),
            'bundle_id': app.bundleIdentifier(),
            'name': app.localizedName(),
        }

    def validate_application(self, pid: int) -> bool:
        app = NSRunningApplication.runningApplicationWithProcessIdentifier_(pid)
        if not app or app.bundleIdentifier() in SecureAXAutomation.BLOCKED_APPS:
            return False
        # Verify code signature
        result = subprocess.run(['codesign', '-v', app.bundleURL().path()], capture_output=True)
        return result.returncode == 0

5. 実装ワークフロー(TDD)

ステップ 1:まず失敗するテストを書く
# tests/test_ax_automation.py
import pytest
from unittest.mock import patch, MagicMock

class TestTCCValidation:
    def test_raises_error_when_permission_missing(self):
        with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=False):
            with pytest.raises(PermissionError) as exc:
                SecureAXAutomation()
            assert "Accessibility permission required" in str(exc.value)

class TestSecureElementDiscovery:
    def test_blocks_keychain_access(self):
        with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=True):
            automation = SecureAXAutomation()
            with pytest.raises(SecurityError):
                automation.get_application_element(pid=1234)  # Keychain PID

    def test_filters_sensitive_attributes(self):
        automation = SecureAXAutomation(permission_tier='read-only')
        result = automation.get_attribute(MagicMock(), 'AXPasswordField')
        assert result == '[REDACTED]'

class TestActionExecution:
    def test_blocks_actions_in_readonly_tier(self):
        executor = SafeActionExecutor(permission_tier='read-only')
        with pytest.raises(PermissionError):
            executor.perform_action(MagicMock(), 'AXPress')
ステップ 2:テストに合格するための最小限の実装

テストに合格するクラスとメソッドを実装します。

ステップ 3:パターンに従ってリファクタリング

セキュリティパターン、キャッシュ、エラー処理を適用します。

ステップ 4:完全な検証を実行
# Run all tests with coverage
pytest tests/ -v --cov=ax_automation --cov-report=term-missing

# Run security-specific tests
pytest tests/test_ax_automation.py -k "security or permission" -v

# Run with timeout to catch hangs
pytest tests/ --timeout=30

6. パフォーマンスパターン

パターン 1:要素のキャッシュ
# BAD: Query repeatedly
element = AXUIElementCreateApplication(pid)  # Each call

# GOOD: Cache with TTL
class ElementCache:
    def __init__(self, ttl=5.0):
        self.cache, self.ttl = {}, ttl

    def get_or_create(self, pid, role):
        key = (pid, role)
        if key in self.cache and time() - self.cache[key][1] < self.ttl:
            return self.cache[key][0]
        element = self._create_element(pid, role)
        self.cache[key] = (element, time())
        return element
パターン 2:範囲の限定
# BAD: Search entire hierarchy
find_all_children(app_element, role='AXButton')  # Deep search

# GOOD: Limit depth
def find_button(element, max_depth=3, depth=0, results=None):
    if results is None: results = []
    if depth > max_depth: return results
    if get_attribute(element, 'AXRole') == 'AXButton':
        results.append(element)
    else:
        for child in get_attribute(element, 'AXChildren') or []:
            find_button(child, max_depth, depth+1, results)
    return results
パターン 3:非同期クエリ
# BAD: Sequential blocking
for app in apps: windows.extend(get_windows(app))

# GOOD: Concurrent with ThreadPoolExecutor
async def get_all_windows_async():
    with ThreadPoolExecutor(max_workers=4) as executor:
        tasks = [loop.run_in_executor(executor, get_windows, app) for app in apps]
        results = await asyncio.gather(*tasks)
    return [w for wins in results for w in wins]
パターン 4:属性の一括取得
# BAD: Multiple calls
title = AXUIElementCopyAttributeValue(element, 'AXTitle', None)
role = AXUIElementCopyAttributeValue(element, 'AXRole', None)

# GOOD: Batch query
error, values = AXUIElementCopyMultipleAttributeValues(
    element, ['AXTitle', 'AXRole', 'AXPosition', 'AXSize'], None
)
info = dict(zip(attributes, values)) if error == kAXErrorSuccess else {}
パターン 5:オブザーバーの最適化
# BAD: Observer for every notification without debounce

# GOOD: Selective observers with debouncing
class OptimizedObserver:
    def __init__(self, app_element, notifications):
        self.last_callback, self.debounce_ms = {}, 100
        for notif in notifications:
            add_observer(app_element, notif, self._debounced_callback)

    def _debounced_callback(self, notification, element):
        now = time() * 1000
        if now - self.last_callback.get(notification, 0) < self.debounce_ms:
            return
        self.last_callback[notification] = now
        self._handle_notification(notification, element)

7. セキュリティ基準

7.1 重大な脆弱性
CVE/CWE深刻度説明対策
CVE-2023-32364CRITICALシンボリックリンクを介した TCC の回避macOS をアップデートし、パスを検証する
CVE-2023-28206HIGHAX 権限の昇格プロセスの検証、コード署名
CWE-290HIGHバンドル ID の偽装コード署名を検証する
CWE-74HIGHAX を介した入力インジェクションSecurityAgent をブロックする
CVE-2022-42796MEDIUMHardened runtime の回避対象アプリのランタイムを検証する
7.2 OWASP との対応
OWASPリスク対策
A01 Broken AccessCRITICALTCC の検証、ブロックリスト
A02 MisconfigurationHIGH最小限の権限
A05 InjectionHIGH入力の検証
A07 Auth FailuresHIGHコード署名の検証
7.3 権限ティアモデル
ティア属性アクションタイムアウト
read-onlyAXTitle、AXRole、AXChildrenなし30s
standardすべてAXPress、AXIncrement60s
elevatedすべてすべて(SecurityAgent を除く)120s

8. よくある間違い

重大なアンチパターン - 必ず避けてください:

  • TCC 権限を確認せずに自動化する
  • バンドル ID だけを信頼する(コード署名を検証する)
  • セキュリティダイアログ(SecurityAgent、Keychain)にアクセスする
  • AX 操作にタイムアウトを設定しない(無期限に停止する可能性があります)
  • TTL なしで要素をキャッシュする(要素が古くなる可能性があります)

9. 実装前チェックリスト

フェーズ 1:コードを書く前
  • TCC 権限の要件を文書化した
  • 対象アプリケーションを特定し、ブロックリストと照合して検証した
  • 権限ティアを決定した(read-only/standard/elevated)
  • 権限検証のテストケースを作成した
  • 要素検出のテストケースを作成した
  • アクション実行のテストケースを作成した
フェーズ 2:実装中
  • TCC 権限の検証を実装した
  • アプリケーションのブロックリストを設定した
  • コード署名の検証を有効にした
  • 権限ティアシステムを適用した
  • 監査ログを有効にした
  • すべての操作にタイムアウトを適用した
  • パフォーマンス向上のために要素のキャッシュを実装した
  • 該当する場合は属性のバッチ処理を使用した
フェーズ 3:コミット前
  • すべての TDD テストが成功する:pytest tests/ -v
  • セキュリティテストが成功する:pytest -k "security or permission"
  • ブロック対象のアプリケーションにアクセスできない
  • タイムアウト処理を確認した
  • 対象の macOS バージョンでテストした
  • サンドボックスとの互換性を確認した
  • Hardened runtime との互換性を確認した
  • コードカバレッジがしきい値を満たしている:pytest --cov --cov-fail-under=80

10. まとめ

目標は、次の特性を備えた macOS アクセシビリティ自動化を作成することです:

  • 安全:TCC の検証、コード署名の検証、アプリケーションのブロックリスト
  • 信頼性:適切なエラー処理、タイムアウトの適用
  • 準拠:macOS のセキュリティモデルとサンドボックス境界を尊重する

セキュリティに関する注意事項:

  1. 自動化の前に、必ず TCC 権限を検証する
  2. バンドル ID だけでなく、コード署名を検証する
  3. セキュリティダイアログや Keychain を自動化しない
  4. すべての操作を相関 ID とともに記録する
  5. macOS のセキュリティ境界を尊重する

参考資料

  • 高度なパターン:references/advanced-patterns.md を参照
  • セキュリティの例:references/security-examples.md を参照
  • 脅威モデル:references/threat-model.md を参照

コメント