How to programmatically set up, modify, and verify OS-level configurations (files, GNOME settings, audio, permissions, system config) using Python. For setup-gen and reward-gen agents.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/xlang-ai/CUA-Gym --skill os
O comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Instruções da origem · Visualização somente leitura
name
os
description
How to programmatically set up, modify, and verify OS-level configurations (files, GNOME settings, audio, permissions, system config) using Python. For setup-gen and reward-gen agents.
user-invocable
false
OS — Python Manipulation Guide
This skill teaches setup-gen (create/modify system state) and reward-gen (read/verify system state) how to work with Linux OS tasks using Python.
# Set timezone to UTC
subprocess.run(["sudo", "timedatectl", "set-timezone", "UTC"], check=True)
# Set to specific timezone
subprocess.run(["sudo", "timedatectl", "set-timezone", "America/New_York"], check=True)
Audio Volume (PulseAudio)
# Set volume to max (100%)
subprocess.run(["pactl", "set-sink-volume", "@DEFAULT_SINK@", "100%"], check=True)
# Set specific volume
subprocess.run(["pactl", "set-sink-volume", "@DEFAULT_SINK@", "75%"], check=True)
# Mute / unmute
subprocess.run(["pactl", "set-sink-mute", "@DEFAULT_SINK@", "0"], check=True) # unmute
defverify_directory_files(dir_path: str, expected_files: set) -> bool:
"""Check that directory contains exactly the expected files."""try:
actual = set(os.listdir(dir_path))
return actual == expected_files
except FileNotFoundError:
returnFalse# Check JPGs were moved to targetdefverify_moved_files(dir_path: str, expected_names: list) -> bool:
actual = set(os.listdir(dir_path))
returnset(expected_names) == actual
Verifying GNOME Settings
defget_gsetting(schema: str, key: str) -> str:
result = subprocess.run(["gsettings", "get", schema, key],
capture_output=True, text=True)
return result.stdout.strip()
# Favorite appsdefverify_favorite_apps(expected: list) -> bool:
raw = get_gsetting("org.gnome.shell", "favorite-apps")
# Output is like: ['app1.desktop', 'app2.desktop']
apps = eval(raw)
returnset(apps) == set(expected)
# Text scalingdefverify_text_enlarged() -> bool:
factor = float(get_gsetting("org.gnome.desktop.interface", "text-scaling-factor"))
return factor > 1.0# Wallpaperdefverify_wallpaper(expected_path: str) -> bool:
uri = get_gsetting("org.gnome.desktop.background", "picture-uri")
return expected_path in uri
Verifying Timezone
defverify_utc_timezone() -> bool:
result = subprocess.run(["timedatectl"], capture_output=True, text=True)
lines = result.stdout.split("\n")
for line in lines:
if"Time zone"in line:
return"+0000)"in line
returnFalse
Verifying Audio Volume
defget_volume() -> int:
"""Get current default sink volume as percentage."""
result = subprocess.run(
["pactl", "get-sink-volume", "@DEFAULT_SINK@"],
capture_output=True, text=True
)
# Output: "Volume: front-left: 65536 / 100% / ..."import re
match = re.search(r'(\d+)%', result.stdout)
returnint(match.group(1)) ifmatchelse -1assert get_volume() == 100
Verifying File Permissions
defverify_permissions(path: str, expected_mode: int) -> bool:
"""expected_mode as octal, e.g. 0o644."""
actual = stat.S_IMODE(os.stat(path).st_mode)
return actual == expected_mode
# Recursive checkdefverify_all_file_permissions(dir_path: str, expected_mode: int) -> bool:
for root, dirs, files in os.walk(dir_path):
for f in files:
ifnot verify_permissions(os.path.join(root, f), expected_mode):
returnFalsereturnTrue
Verifying Command Output (include/exclude)
defverify_command_output(command: str, include: list = None, exclude: list = None) -> bool:
"""Run a shell command and check output contains/excludes strings."""
result = subprocess.run(command, shell=True, capture_output=True, text=True)
output = result.stdout + result.stderr
if include andnotall(s in output for s in include):
returnFalseif exclude andany(s in output for s in exclude):
returnFalsereturnTrue# Examplesassert verify_command_output("which spotify", include=["spotify"], exclude=["not found"])
assert verify_command_output("stty size", include=["43 132"])
defverify_file_contains(path: str, expected_strings: list) -> bool:
try:
withopen(path, "r") as f:
content = f.read()
returnall(s in content for s in expected_strings)
except FileNotFoundError:
returnFalsedefverify_text_file_match(actual: str, expected: str,
ignore_blanks=False, ignore_case=False) -> bool:
"""Compare two text files with optional normalization."""import re
withopen(actual) as f:
a = f.read()
withopen(expected) as f:
e = f.read()
if ignore_blanks:
a = re.sub(r'[\t\n]', ' ', a).strip()
a = re.sub(r'\s+', ' ', a)
e = re.sub(r'[\t\n]', ' ', e).strip()
e = re.sub(r'\s+', ' ', e)
if ignore_case:
a, e = a.lower(), e.lower()
return a == e
Verifying JSON Configuration
defverify_json_subset(file_path: str, expected: dict) -> bool:
"""Check that expected is a subset of the JSON file."""try:
withopen(file_path) as f:
actual = json.load(f)
return _is_subset(expected, actual)
except (FileNotFoundError, json.JSONDecodeError):
returnFalsedef_is_subset(expected, actual) -> bool:
ifisinstance(expected, dict):
ifnotisinstance(actual, dict):
returnFalsereturnall(k in actual and _is_subset(v, actual[k]) for k, v in expected.items())
ifisinstance(expected, list):
return expected == actual
return expected == actual
3. Bitter Lessons
gsettings get output is GVariant, not JSON. It uses single quotes: ['app.desktop']. Use eval() to parse Python-like syntax, or strip and parse manually. Do NOT use json.loads().
PulseAudio volume output format varies.pactl get-sink-volume outputs front-left: 65536 / 100% / 0.00 dB. Extract the percentage with regex, don't split on spaces — the format changes across versions.
stty size returns rows cols, NOT cols rows. The output is 43 132 meaning 43 rows, 132 columns. Reversing them is a common error.
File permission comparison must use stat.S_IMODE.os.stat().st_mode includes file type bits. Always mask with stat.S_IMODE() to get just permission bits (e.g., 0o644).
subprocess.run with shell=True vs argument list. Use shell=True for pipes and redirects ("ls | grep foo"). Use argument list ["ls", "-la"] for simple commands — safer and no shell injection risk.
timedatectl timezone line format: Time zone: Atlantic/Faroe (WET, +0000). Parse the offset from the end of the line. Check for +0000) suffix for UTC verification, not just the timezone name (multiple zones map to UTC+0).
GNOME dconf paths have UUIDs. Terminal profile paths like /org/gnome/terminal/legacy/profiles:/:b1dcc9dd-.../ contain a profile UUID that varies by system. Query the default profile first: gsettings get org.gnome.terminal.legacy.profiles: default.
shutil.copy vs shutil.copy2 vs shutil.copytree.copy preserves only permissions. copy2 preserves metadata (timestamps). copytree is for directories. Using the wrong one causes metadata-based verification to fail.
os.makedirs needs exist_ok=True. Without it, creating a directory that already exists raises FileExistsError. Always use exist_ok=True in setup scripts.
which returns exit code 1 when not found. Don't check result.stdout alone — also check result.returncode == 0. An empty stdout with returncode 0 is also possible if the binary has no output.
Setup scripts using sudo may prompt for password. In VM environments, configure NOPASSWD in sudoers or use echo password | sudo -S. Never assume passwordless sudo without checking.
Snap-installed apps may not appear in which. Snap uses /snap/bin/ which may not be in PATH. Check /snap/bin/<app> directly or use snap list to verify.