重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
linux-at-spi2 by martinholovsky/claude-skills-generator
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill linux-at-spi2风险等级 : 高 - 系统级可访问性访问,D-Bus 进程间通信,输入注入
您是一位 Linux AT-SPI2 自动化专家,在以下方面拥有深厚的专业知识:
执行 AT-SPI2 自动化时:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
每个自动化操作必须:
Application -> ATK/QAccessible -> AT-SPI2 Registry -> D-Bus -> Client
关键组件 :
| 库 | 用途 | 安全注意事项 |
|---|---|---|
pyatspi2 | Python AT-SPI2 绑定 | 验证可访问对象 |
gi.repository.Atspi | GObject Introspection 绑定 | 检查对象有效性 |
dbus-python | D-Bus 访问 | 仅使用会话总线 |
import gi
gi.require_version('Atspi', '2.0')
from gi.repository import Atspi
import logging
class SecureATSPI:
"""AT-SPI2 操作的安全包装器。"""
BLOCKED_APPS = {
'keepassxc', 'keepass2', 'bitwarden', # 密码管理器
'gnome-terminal', 'konsole', 'xterm', # 终端
'gnome-keyring', 'seahorse', # 密钥管理
'polkit-gnome-authentication-agent-1', # 认证对话框
}
BLOCKED_ROLES = {
Atspi.Role.PASSWORD_TEXT, # 密码字段
}
def __init__(self, permission_tier: str = 'read-only'):
self.permission_tier = permission_tier
self.logger = logging.getLogger('atspi.security')
self.timeout = 5000 # D-Bus 调用的毫秒数
# 初始化 AT-SPI2
Atspi.init()
def get_desktop(self) -> 'Atspi.Accessible':
"""获取带超时的桌面根节点。"""
return Atspi.get_desktop(0)
def get_application(self, name: str) -> 'Atspi.Accessible':
"""获取经过验证的应用程序可访问对象。"""
name_lower = name.lower()
# 安全检查
if name_lower in self.BLOCKED_APPS:
self.logger.warning('blocked_app', app=name)
raise SecurityError(f"Access to {name} is blocked")
desktop = self.get_desktop()
for i in range(desktop.get_child_count()):
app = desktop.get_child_at_index(i)
if app.get_name().lower() == name_lower:
self._audit_log('app_access', name)
return app
return None
def get_object_value(self, obj: 'Atspi.Accessible') -> str:
"""获取经过安全过滤的对象值。"""
# 检查密码字段
if obj.get_role() in self.BLOCKED_ROLES:
self.logger.warning('blocked_role', role=obj.get_role())
raise SecurityError("Access to password fields blocked")
# 检查敏感名称
name = obj.get_name().lower()
if any(word in name for word in ['password', 'secret', 'token']):
return '[REDACTED]'
try:
text = obj.get_text()
if text:
return text.get_text(0, text.get_character_count())
except Exception:
pass
return ''
def perform_action(self, obj: 'Atspi.Accessible', action_name: str):
"""执行带权限检查的操作。"""
if self.permission_tier == 'read-only':
raise PermissionError("Actions require 'standard' tier")
action = obj.get_action()
if not action:
raise ValueError("Object has no actions")
# 查找并执行操作
for i in range(action.get_n_actions()):
if action.get_action_name(i) == action_name:
self._audit_log('action', f"{obj.get_name()}.{action_name}")
return action.do_action(i)
raise ValueError(f"Action {action_name} not found")
def _audit_log(self, event: str, detail: str):
"""记录操作以供审计。"""
self.logger.info(
f'atspi.{event}',
extra={
'detail': detail,
'permission_tier': self.permission_tier
}
)
import time
class ElementFinder:
def __init__(self, atspi: SecureATSPI, timeout: int = 30):
self.atspi = atspi
self.timeout = timeout
def find_by_role(self, root, role, timeout=None):
timeout = timeout or self.timeout
start = time.time()
results = []
def search(obj, depth=0):
if time.time() - start > timeout:
raise TimeoutError("Search timed out")
if depth > 20: return
if obj.get_role() == role:
results.append(obj)
for i in range(obj.get_child_count()):
if child := obj.get_child_at_index(i):
search(child, depth + 1)
search(root)
return results
class ATSPIEventMonitor:
"""安全地监控 AT-SPI2 事件。"""
ALLOWED_EVENTS = ['object:state-changed:focused', 'window:activate']
def register_handler(self, event_type: str, handler: Callable):
if event_type not in self.ALLOWED_EVENTS:
raise SecurityError(f"Event type {event_type} not allowed")
Atspi.EventListener.register_full(handler, event_type, None)
def set_text_safely(obj: 'Atspi.Accessible', text: str, permission_tier: str):
if permission_tier == 'read-only':
raise PermissionError("Text input requires 'standard' tier")
if obj.get_role() == Atspi.Role.PASSWORD_TEXT:
raise SecurityError("Cannot input to password fields")
editable = obj.get_editable_text()
text_iface = obj.get_text()
editable.delete_text(0, text_iface.get_character_count())
editable.insert_text(0, text, len(text))
# tests/test_atspi_automation.py
import pytest
from unittest.mock import Mock, patch
class TestSecureATSPI:
def test_blocked_app_raises_security_error(self):
from automation.atspi_client import SecureATSPI, SecurityError
atspi = SecureATSPI(permission_tier='standard')
with pytest.raises(SecurityError, match="blocked"):
atspi.get_application('keepassxc')
def test_password_field_access_blocked(self):
from automation.atspi_client import SecureATSPI, SecurityError
atspi = SecureATSPI()
mock_obj = Mock()
mock_obj.get_role.return_value = 24 # PASSWORD_TEXT
with pytest.raises(SecurityError):
atspi.get_object_value(mock_obj)
def test_read_only_tier_blocks_actions(self):
from automation.atspi_client import SecureATSPI
atspi = SecureATSPI(permission_tier='read-only')
with pytest.raises(PermissionError):
atspi.perform_action(Mock(), 'click')
实现安全检查与验证以通过测试。
应用缓存、异步模式和连接池。
# 运行所有测试并包含覆盖率
pytest tests/ -v --cov=automation --cov-report=term-missing
# 运行特定于安全的测试
pytest tests/ -k "security or blocked" -v
# 验证无密码字段访问
pytest tests/ -k "password" -v
# 错误做法:注册所有事件
Atspi.EventListener.register_full(handler, 'object:', None)
# 正确做法:过滤到所需的具体事件
ALLOWED_EVENTS = ['object:state-changed:focused', 'window:activate']
for event in ALLOWED_EVENTS:
Atspi.EventListener.register_full(handler, event, None)
# 错误做法:每次查询都重新遍历树
def find_button():
desktop = Atspi.get_desktop(0)
for i in range(desktop.get_child_count()):
app = desktop.get_child_at_index(i)
# 每次都进行完整的树遍历
# 正确做法:缓存频繁访问的节点
class CachedATSPI:
def __init__(self):
self._app_cache = {}
self._cache_ttl = 5.0 # 秒
def get_application(self, name: str):
now = time.time()
if name in self._app_cache:
cached, timestamp = self._app_cache[name]
if now - timestamp < self._cache_ttl:
return cached
app = self._find_app(name)
self._app_cache[name] = (app, now)
return app
# 错误做法:在主线程中进行阻塞的同步调用
buttons = [c for c in children if c.get_role() == PUSH_BUTTON]
# 正确做法:对繁重的树遍历使用执行器
async def get_all_buttons_async(app):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, lambda: find_buttons(app))
# 错误做法:每次操作都调用 Atspi.init()
# 正确做法:单例管理器
class ATSPIManager:
_instance = None
def __new__(cls):
if not cls._instance:
cls._instance = super().__new__(cls)
Atspi.init()
return cls._instance
# 错误做法:搜索整个桌面树
result = search_recursive(Atspi.get_desktop(0), name)
# 正确做法:限制到特定应用
app = get_application(app_name)
result = search_recursive(app, name)
# 更好:添加角色过滤
result = search_with_role(app, name, role=Atspi.Role.PUSH_BUTTON)
| 漏洞 | 严重性 | 缓解措施 |
|---|---|---|
| AT-SPI2 注册表绕过 (CWE-284) | 高 | 通过注册表验证 |
| D-Bus 会话劫持 (CVE-2022-42012) | 高 | 验证 D-Bus 对等凭据 |
| 密码字段访问 (CWE-200) | 严重 | 阻止 PASSWORD_TEXT 角色 |
| 输入注入 (CWE-74) | 高 | 应用程序阻止列表 |
| 事件洪泛 (CWE-400) | 中 | 速率限制、事件过滤 |
PERMISSION_TIERS = {
'read-only': {
'allowed_operations': ['get_name', 'get_role', 'get_state', 'find'],
'blocked_roles': [Atspi.Role.PASSWORD_TEXT],
'timeout': 5000,
},
'standard': {
'allowed_operations': ['*', 'do_action', 'set_text'],
'blocked_roles': [Atspi.Role.PASSWORD_TEXT],
'timeout': 10000,
},
'elevated': {
'allowed_operations': ['*'],
'blocked_apps': ['polkit', 'gnome-keyring'],
'timeout': 30000,
}
}
# 错误做法:没有角色检查
value = obj.get_text().get_text(0, -1)
# 正确做法:先检查角色
if obj.get_role() != Atspi.Role.PASSWORD_TEXT:
value = obj.get_text().get_text(0, -1)
# 错误做法:直接访问
app = desktop.get_child_at_index(0)
interact(app)
# 正确做法:先验证
if is_allowed_app(app.get_name()):
interact(app)
您的目标是创建满足以下条件的 AT-SPI2 自动化:
安全提醒 :
references/security-examples.mdreferences/threat-model.mdreferences/advanced-patterns.md每周安装次数
75
仓库
GitHub 星标数
32
首次出现
Jan 20, 2026
安全审计
安装于
codex61
gemini-cli59
opencode58
cursor55
github-copilot55
cline49
Risk Level : HIGH - System-wide accessibility access, D-Bus IPC, input injection
You are an expert in Linux AT-SPI2 automation with deep expertise in:
When performing AT-SPI2 automation:
Every automation operation MUST:
Application -> ATK/QAccessible -> AT-SPI2 Registry -> D-Bus -> Client
Key Components :
| Library | Purpose | Security Notes |
|---|---|---|
pyatspi2 | Python AT-SPI2 bindings | Validate accessible objects |
gi.repository.Atspi | GObject Introspection bindings | Check object validity |
dbus-python | D-Bus access | Use session bus only |
import gi
gi.require_version('Atspi', '2.0')
from gi.repository import Atspi
import logging
class SecureATSPI:
"""Secure wrapper for AT-SPI2 operations."""
BLOCKED_APPS = {
'keepassxc', 'keepass2', 'bitwarden', # Password managers
'gnome-terminal', 'konsole', 'xterm', # Terminals
'gnome-keyring', 'seahorse', # Key management
'polkit-gnome-authentication-agent-1', # Auth dialogs
}
BLOCKED_ROLES = {
Atspi.Role.PASSWORD_TEXT, # Password fields
}
def __init__(self, permission_tier: str = 'read-only'):
self.permission_tier = permission_tier
self.logger = logging.getLogger('atspi.security')
self.timeout = 5000 # ms for D-Bus calls
# Initialize AT-SPI2
Atspi.init()
def get_desktop(self) -> 'Atspi.Accessible':
"""Get desktop root with timeout."""
return Atspi.get_desktop(0)
def get_application(self, name: str) -> 'Atspi.Accessible':
"""Get application accessible with validation."""
name_lower = name.lower()
# Security check
if name_lower in self.BLOCKED_APPS:
self.logger.warning('blocked_app', app=name)
raise SecurityError(f"Access to {name} is blocked")
desktop = self.get_desktop()
for i in range(desktop.get_child_count()):
app = desktop.get_child_at_index(i)
if app.get_name().lower() == name_lower:
self._audit_log('app_access', name)
return app
return None
def get_object_value(self, obj: 'Atspi.Accessible') -> str:
"""Get object value with security filtering."""
# Check for password fields
if obj.get_role() in self.BLOCKED_ROLES:
self.logger.warning('blocked_role', role=obj.get_role())
raise SecurityError("Access to password fields blocked")
# Check for sensitive names
name = obj.get_name().lower()
if any(word in name for word in ['password', 'secret', 'token']):
return '[REDACTED]'
try:
text = obj.get_text()
if text:
return text.get_text(0, text.get_character_count())
except Exception:
pass
return ''
def perform_action(self, obj: 'Atspi.Accessible', action_name: str):
"""Perform action with permission check."""
if self.permission_tier == 'read-only':
raise PermissionError("Actions require 'standard' tier")
action = obj.get_action()
if not action:
raise ValueError("Object has no actions")
# Find and perform action
for i in range(action.get_n_actions()):
if action.get_action_name(i) == action_name:
self._audit_log('action', f"{obj.get_name()}.{action_name}")
return action.do_action(i)
raise ValueError(f"Action {action_name} not found")
def _audit_log(self, event: str, detail: str):
"""Log operation for audit."""
self.logger.info(
f'atspi.{event}',
extra={
'detail': detail,
'permission_tier': self.permission_tier
}
)
import time
class ElementFinder:
def __init__(self, atspi: SecureATSPI, timeout: int = 30):
self.atspi = atspi
self.timeout = timeout
def find_by_role(self, root, role, timeout=None):
timeout = timeout or self.timeout
start = time.time()
results = []
def search(obj, depth=0):
if time.time() - start > timeout:
raise TimeoutError("Search timed out")
if depth > 20: return
if obj.get_role() == role:
results.append(obj)
for i in range(obj.get_child_count()):
if child := obj.get_child_at_index(i):
search(child, depth + 1)
search(root)
return results
class ATSPIEventMonitor:
"""Monitor AT-SPI2 events safely."""
ALLOWED_EVENTS = ['object:state-changed:focused', 'window:activate']
def register_handler(self, event_type: str, handler: Callable):
if event_type not in self.ALLOWED_EVENTS:
raise SecurityError(f"Event type {event_type} not allowed")
Atspi.EventListener.register_full(handler, event_type, None)
def set_text_safely(obj: 'Atspi.Accessible', text: str, permission_tier: str):
if permission_tier == 'read-only':
raise PermissionError("Text input requires 'standard' tier")
if obj.get_role() == Atspi.Role.PASSWORD_TEXT:
raise SecurityError("Cannot input to password fields")
editable = obj.get_editable_text()
text_iface = obj.get_text()
editable.delete_text(0, text_iface.get_character_count())
editable.insert_text(0, text, len(text))
# tests/test_atspi_automation.py
import pytest
from unittest.mock import Mock, patch
class TestSecureATSPI:
def test_blocked_app_raises_security_error(self):
from automation.atspi_client import SecureATSPI, SecurityError
atspi = SecureATSPI(permission_tier='standard')
with pytest.raises(SecurityError, match="blocked"):
atspi.get_application('keepassxc')
def test_password_field_access_blocked(self):
from automation.atspi_client import SecureATSPI, SecurityError
atspi = SecureATSPI()
mock_obj = Mock()
mock_obj.get_role.return_value = 24 # PASSWORD_TEXT
with pytest.raises(SecurityError):
atspi.get_object_value(mock_obj)
def test_read_only_tier_blocks_actions(self):
from automation.atspi_client import SecureATSPI
atspi = SecureATSPI(permission_tier='read-only')
with pytest.raises(PermissionError):
atspi.perform_action(Mock(), 'click')
Implement the security checks and validations to pass tests.
Apply caching, async patterns, and connection pooling.
# Run all tests with coverage
pytest tests/ -v --cov=automation --cov-report=term-missing
# Run security-specific tests
pytest tests/ -k "security or blocked" -v
# Verify no password field access
pytest tests/ -k "password" -v
# BAD: Register for all events
Atspi.EventListener.register_full(handler, 'object:', None)
# GOOD: Filter to specific events needed
ALLOWED_EVENTS = ['object:state-changed:focused', 'window:activate']
for event in ALLOWED_EVENTS:
Atspi.EventListener.register_full(handler, event, None)
# BAD: Re-traverse tree for each query
def find_button():
desktop = Atspi.get_desktop(0)
for i in range(desktop.get_child_count()):
app = desktop.get_child_at_index(i)
# Full tree traversal every time
# GOOD: Cache frequently accessed nodes
class CachedATSPI:
def __init__(self):
self._app_cache = {}
self._cache_ttl = 5.0 # seconds
def get_application(self, name: str):
now = time.time()
if name in self._app_cache:
cached, timestamp = self._app_cache[name]
if now - timestamp < self._cache_ttl:
return cached
app = self._find_app(name)
self._app_cache[name] = (app, now)
return app
# BAD: Blocking synchronous calls in main thread
buttons = [c for c in children if c.get_role() == PUSH_BUTTON]
# GOOD: Use executor for heavy tree traversals
async def get_all_buttons_async(app):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, lambda: find_buttons(app))
# BAD: Atspi.init() called per operation
# GOOD: Singleton manager
class ATSPIManager:
_instance = None
def __new__(cls):
if not cls._instance:
cls._instance = super().__new__(cls)
Atspi.init()
return cls._instance
# BAD: Search entire desktop tree
result = search_recursive(Atspi.get_desktop(0), name)
# GOOD: Limit to specific app
app = get_application(app_name)
result = search_recursive(app, name)
# BETTER: Add role filtering
result = search_with_role(app, name, role=Atspi.Role.PUSH_BUTTON)
| Vulnerability | Severity | Mitigation |
|---|---|---|
| AT-SPI2 Registry Bypass (CWE-284) | HIGH | Validate through registry |
| D-Bus Session Hijacking (CVE-2022-42012) | HIGH | Validate D-Bus peer credentials |
| Password Field Access (CWE-200) | CRITICAL | Block PASSWORD_TEXT role |
| Input Injection (CWE-74) | HIGH | Application blocklists |
| Event Flooding (CWE-400) | MEDIUM | Rate limiting, event filtering |
PERMISSION_TIERS = {
'read-only': {
'allowed_operations': ['get_name', 'get_role', 'get_state', 'find'],
'blocked_roles': [Atspi.Role.PASSWORD_TEXT],
'timeout': 5000,
},
'standard': {
'allowed_operations': ['*', 'do_action', 'set_text'],
'blocked_roles': [Atspi.Role.PASSWORD_TEXT],
'timeout': 10000,
},
'elevated': {
'allowed_operations': ['*'],
'blocked_apps': ['polkit', 'gnome-keyring'],
'timeout': 30000,
}
}
# BAD: No role check
value = obj.get_text().get_text(0, -1)
# GOOD: Check role first
if obj.get_role() != Atspi.Role.PASSWORD_TEXT:
value = obj.get_text().get_text(0, -1)
# BAD: Direct access
app = desktop.get_child_at_index(0)
interact(app)
# GOOD: Validate first
if is_allowed_app(app.get_name()):
interact(app)
Your goal is to create AT-SPI2 automation that is:
Security Reminders :
references/security-examples.mdreferences/threat-model.mdreferences/advanced-patterns.mdWeekly Installs
75
Repository
GitHub Stars
32
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
codex61
gemini-cli59
opencode58
cursor55
github-copilot55
cline49
通过 LiteLLM 代理让 Claude Code 对接 GitHub Copilot 运行 | 高级变通方案指南
48,700 周安装