| name | linux-at-spi2 |
| version | 2.0.0 |
| description | Linux accessibility automation with AT-SPI2 for GTK/Qt application testing, D-Bus a11y, and UI control. Use when automating Linux desktop apps via AT-SPI or accessibility APIs. Do NOT use for macOS automation (use macos-accessibility). |
| risk_level | MEDIUM |
| token_budget | 2500 |
Linux AT-SPI2 - Code Generation Rules
0. Anti-Hallucination Protocol
0.2 Security Patterns (security rules)
CWE-284: D-Bus Access Control
- Do not: Open D-Bus accessibility to all users
- Instead: Restrict to session bus, validate caller
CWE-78: Command Injection via Accessibility
- Do not: Execute commands from accessibility text content
- Instead: Treat all accessibility data as untrusted input
1. Security Principles
1.1 D-Bus Access Control (CWE-284)
Principle: AT-SPI2 operates over D-Bus session bus. Validate all received data as untrusted IPC.
def handle_accessible(proxy):
name = proxy.Name
exec(f"log_{name}()")
import re
from gi.repository import Atspi
SAFE_NAME_PATTERN = re.compile(r'^[\w\s\-\.]{1,256}$')
def handle_accessible(accessible: Atspi.Accessible) -> str | None:
try:
name = accessible.get_name()
if name and SAFE_NAME_PATTERN.match(name):
return name
return None
except GLib.Error:
return None
1.2 Resource Exhaustion Prevention (CWE-400)
Principle: AT-SPI trees can be massive. Always limit traversal depth and implement timeouts.
def find_all_buttons(root):
buttons = []
for child in root:
buttons.extend(find_all_buttons(child))
return buttons
from gi.repository import Atspi, GLib
MAX_DEPTH = 15
MAX_NODES = 5000
def find_buttons_bounded(
accessible: Atspi.Accessible,
depth: int = 0,
visited: set[int] | None = None
) -> list[Atspi.Accessible]:
if visited is None:
visited = set()
if depth > MAX_DEPTH or len(visited) > MAX_NODES:
return []
unique_id = accessible.get_index_in_parent()
if unique_id in visited:
return []
visited.add(unique_id)
buttons = []
role = accessible.get_role()
if role == Atspi.Role.PUSH_BUTTON:
buttons.append(accessible)
try:
child_count = accessible.get_child_count()
for i in range(min(child_count, 100)):
child = accessible.get_child_at_index(i)
if child:
buttons.extend(find_buttons_bounded(child, depth + 1, visited))
except GLib.Error:
pass
return buttons
1.3 Privilege Separation (CWE-250)
Principle: AT-SPI2 requires accessibility permissions. Run automation in isolated process.
2. Version Requirements
# AT-SPI2 (via PyGObject)
PyGObject>=3.42.0
# Atspi bindings (system package)
libatspi>=2.46
# GLib for main loop
glib>=2.74
3. Code Patterns
WHEN querying accessibility tree, use Atspi bindings with GLib main loop
from gi.repository import Atspi
desktop = Atspi.get_desktop(0)
app = desktop.get_child_at_index(0)
from gi.repository import Atspi, GLib
class ATSPIClient:
def __init__(self):
Atspi.init()
self.loop = GLib.MainLoop()
self.result: list[Atspi.Accessible] = []
def find_by_role(
self,
role: Atspi.Role,
timeout_ms: int = 5000
) -> list[Atspi.Accessible]:
self.result = []
def search():
desktop = Atspi.get_desktop(0)
for i in range(desktop.get_child_count()):
app = desktop.get_child_at_index(i)
if app:
self._search_recursive(app, role, depth=0)
self.loop.quit()
return False
GLib.timeout_add(0, search)
GLib.timeout_add(timeout_ms, self.loop.quit)
self.loop.run()
return self.result
def _search_recursive(
self,
node: Atspi.Accessible,
target_role: Atspi.Role,
depth: int
):
if depth > 10 or len(self.result) > 100:
return
try:
if node.get_role() == target_role:
self.result.append(node)
for i in range(min(node.get_child_count(), 50)):
child = node.get_child_at_index(i)
if child:
self._search_recursive(child, target_role, depth + 1)
except GLib.Error:
pass
WHEN performing actions, verify state before acting
def click_button(button):
action = button.get_action_iface()
action.do_action(0)
from gi.repository import Atspi, GLib
def safe_click(accessible: Atspi.Accessible) -> bool:
"""Click an accessible element safely."""
try:
states = accessible.get_state_set()
if not states.contains(Atspi.StateType.VISIBLE):
return False
if not states.contains(Atspi.StateType.SENSITIVE):
return False
action = accessible.get_action_iface()
if not action:
return False
n_actions = action.get_n_actions()
for i in range(n_actions):
action_name = action.get_action_name(i)
if action_name in ('click', 'press', 'activate'):
return action.do_action(i)
return False
except GLib.Error as e:
print(f"Action failed: {e}")
return False
WHEN listening to events, use proper event subscription
while True:
check_for_changes()
time.sleep(0.1)
from gi.repository import Atspi, GLib
from typing import Callable
class ATSPIEventListener:
def __init__(self):
Atspi.init()
self.loop = GLib.MainLoop()
self._listeners: list[Atspi.EventListener] = []
def on_focus_change(
self,
callback: Callable[[Atspi.Accessible], None]
):
def handler(event: Atspi.Event):
if event.source:
callback(event.source)
listener = Atspi.EventListener.new(handler)
listener.register('focus:')
self._listeners.append(listener)
def on_window_create(
self,
callback: Callable[[Atspi.Accessible], None]
):
def handler(event: Atspi.Event):
if event.source:
callback(event.source)
listener = Atspi.EventListener.new(handler)
listener.register('window:create')
self._listeners.append(listener)
def start(self):
self.loop.run()
def stop(self):
for listener in self._listeners:
listener.deregister('focus:')
listener.deregister('window:create')
self.loop.quit()
listener = ATSPIEventListener()
listener.on_focus_change(lambda acc: print(f"Focus: {acc.get_name()}"))
listener.start()
WHEN finding elements by text, use match rules
def find_by_name(root, name):
if root.get_name() == name:
return root
from gi.repository import Atspi
def find_by_match_rule(
root: Atspi.Accessible,
name: str | None = None,
role: Atspi.Role | None = None,
states: list[Atspi.StateType] | None = None
) -> list[Atspi.Accessible]:
"""Find elements using AT-SPI match rules."""
state_set = Atspi.StateSet.new([])
if states:
for state in states:
state_set.add(state)
attributes = GLib.HashTable.new(None, None)
rule = Atspi.MatchRule.new(
state_set,
Atspi.CollectionMatchType.ALL,
attributes,
Atspi.CollectionMatchType.ANY,
[role] if role else [],
Atspi.CollectionMatchType.ANY,
[],
Atspi.CollectionMatchType.ANY,
False
)
collection = root.get_collection_iface()
if not collection:
return []
matches = collection.get_matches(
rule,
Atspi.CollectionSortOrder.CANONICAL,
100,
False
)
if name:
return [m for m in matches if m.get_name() == name]
return matches
4. Anti-Patterns
Do not:
- Traverse accessibility trees without depth limits
- Assume D-Bus responses are immediate (use timeouts)
- Hold references to stale Accessible objects
- Poll for changes instead of using event listeners
- Ignore GLib.Error exceptions from D-Bus calls
- Run automation without accessibility permissions enabled
5. Testing
import pytest
from unittest.mock import Mock, patch
from gi.repository import Atspi, GLib
class TestATSPIClient:
@pytest.fixture
def mock_desktop(self):
6. Pre-Generation Checklist
Before generating AT-SPI2 code: