You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document specifies the per-OS behavior contracts that LocalPCShim must
uphold and the assumptions the platform side may make. It complements
PROTOCOL.md (wire format), SECURITY.md (per-OS
attack surface), and PLATFORM_HOOKS.md (adapter work on
the platform side).
The shim's job is to make every supported OS look the same to the platform
adapter. Where that's impossible, the shim advertises what it actually supports
in HELLO.capabilities and the platform degrades gracefully.
1. Supported OS Matrix
OS
Architecture
Tier
Notes
macOS 13+ (Ventura/Sonoma/Sequoia)
arm64 (Apple Silicon)
Tier 1
Primary dev target. Computer use requires Accessibility + Screen Recording TCC grant.
macOS 13+
x86_64 (Intel)
Tier 1
Same code path as arm64; Rosetta not required.
Windows 10 / 11
x86_64
Tier 1
Computer use works on Win32 desktop; UAC-elevated windows are uncontrollable from a non-elevated shim.
Ubuntu 22.04 / 24.04, Debian 12
x86_64
Tier 1
Computer use works on X11. Wayland sessions block input injection — shim advertises computer_use only when XDG_SESSION_TYPE=x11.
Ubuntu / Debian
arm64
Tier 2
Same code path; not all hardware features tested.
Fedora 39+ / RHEL 9+
x86_64
Tier 2
Same code path as Ubuntu. Wayland is default — same X11 caveat applies.
WSL2 (Ubuntu/Debian on Windows)
x86_64
Tier 2
Treated as Linux. /mnt/c/... paths work but cross the Windows/Linux permission boundary; see WSL2 section. No computer use (no display).
Windows on ARM
arm64
Best-effort
Untested. Python 3.11+ runs natively; pyautogui should work.
Other Linux distros (Arch, openSUSE, Alpine, NixOS)
x86_64 / arm64
Best-effort
Should work via pipx. Keychain backend depends on installed Secret Service implementation.
FreeBSD, OpenBSD, illumos
any
Unsupported
Out of scope.
Tier definitions:
Tier 1: CI runs the test suite; release blockers if broken.
Tier 2: CI smoke-tests; bugs accepted but releases don't block.
Best-effort: Code paths exist; community contributions welcome.
2. Cross-OS Dimensions
One row per dimension. n/a means "doesn't apply on this OS." same means
"identical behavior to the cell to the left."
Path handling
Dimension
macOS
Linux
Windows
Notes
Path separator
/
/
\ (also accepts /)
Shim normalizes incoming wire paths with pathlib.PurePath.
Drive letters / UNC
n/a
n/a
C:\, \\server\share
Path jail must compare on the same drive; cross-drive paths reject.
Reserved filenames
none
none
CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9 (case-insensitive, with or without extension)
Reject in FILE_WRITE; reject reads if the name is \\?\-escaped to a real reserved device.
Invalid path chars
: (legacy HFS quirk; modern APFS allows)
none
< > : " | ? * and control chars 0–31
Reject on FILE_WRITE before opening.
Max path length
1024 (PATH_MAX)
4096 (PATH_MAX)
260 (MAX_PATH) unless \\?\ prefix → 32767
Shim transparently uses \\?\ on Windows for paths > 240 chars.
Alternate data streams
n/a
n/a
file.txt:hidden (NTFS ADS)
Path jail strips/rejects any path containing : after the drive component.
Case sensitivity (default FS)
Insensitive (APFS default)
Sensitive (ext4/btrfs/xfs)
Insensitive (NTFS default)
Path jail compares case-folded on macOS+Windows; raw-byte on Linux. See Path Jail Strategy.
Home dir env var
$HOME
$HOME
%USERPROFILE% (also %HOMEDRIVE%%HOMEPATH%)
Shim resolves ~ via pathlib.Path.home() which handles all three.
Symlinks, junctions, and reparse points
Dimension
macOS
Linux
Windows
Notes
Symbolic links
Yes (ln -s)
Yes
Yes (NTFS, requires SeCreateSymbolicLinkPrivilege or developer mode)
Not resolved by realpath; treated as part of the path tree.
Canonicalize call
os.path.realpath
os.path.realpath
os.path.realpath (Python 3.8+ resolves junctions)
Always run realpath before path-jail comparison.
Remote directory-browser roots
The persistent control channel starts at virtual host roots rather than
accepting a path from the platform.
Platform
Roots exposed
macOS
Canonical home plus accessible real local data volumes immediately under /Volumes; system/startup volumes are excluded.
Linux
Canonical home plus accessible user mounts immediately under /media/$USER and /run/media/$USER; /, /proc, /sys, /dev, and general /run are never roots.
WSL2
Linux home/user mounts plus accessible one-letter fixed-drive mounts immediately under /mnt.
Windows
Canonical %USERPROFILE% plus accessible fixed/removable drive roots; remote, optical, recovery/system, and reparse roots are excluded.
Each request lists one immediate level with fixed host-side scan/response caps.
Symlinks, junctions, inaccessible entries, files, and invalid wire names are
skipped. The result includes directory display names and canonical absolute
paths only; navigation and selection use short-lived opaque references.
Shell selection
Dimension
macOS
Linux
Windows
Notes
Default interactive shell
zsh (macOS 10.15+)
bash (most distros)
cmd.exe (no PowerShell default)
Shim's default for EXECUTE_COMMAND when shell == "auto".
Available shells
sh, bash, zsh
sh, bash, possibly zsh/fish
cmd, powershell, pwsh (if installed)
shell enum on the wire selects explicitly. See PROTOCOL.md.
POSIX bash -c "..." shape
Works
Works
Not available unless Git Bash / WSL installed
Platform must not assume bash on Windows — use argv array or shell: "auto".
Argument quoting
shlex.quote
shlex.quote
subprocess.list2cmdline
Shim uses the right quoter per OS when given argv form.
Process control & signals
Dimension
macOS
Linux
Windows
Notes
Graceful terminate
SIGTERM
SIGTERM
CTRL_BREAK_EVENT for console apps; TerminateProcess for GUI apps
UAC: non-elevated shim cannot inject into elevated windows
autogpt-shim doctor proactively surfaces the macOS dialog via AXIsProcessTrustedWithOptions({AXTrustedCheckOptionPrompt: True}); the daemon refuses to bind WS (exit 78) when computer-use is requested but consent is missing, and launchd's KeepAlive: SuccessfulExit=false re-launches it after the user grants consent. Revocation mid-session returns PERMISSION_PENDING on the next AX-dependent op; the platform LLM retries on the next turn. See COMPUTER_USE.md §Q5.
DPI scaling
Logical points; shim multiplies by backing scale factor
1:1 typically
n/a
Per-monitor DPI; shim sets process DPI aware via SetProcessDpiAwareness(2)
Coordinates on the wire are always physical pixels.
systemd user unit (~/.config/systemd/user/autogpt-shim.service)
Task Scheduler \AutoGPT\Shim (XML at %LOCALAPPDATA%\autogpt-local-executor\autogpt-shim.xml, registered via schtasks /Create /XML ...)
autogpt-shim install writes the file; the user registers it with the OS service manager (the CLI prints the exact command). autogpt-shim uninstall reverses it.
All require user opt-in. CLI prints the systemctl / launchctl / schtasks commands but never runs them. The macOS SuccessfulExit=false flag is load-bearing: it powers the TCC re-launch chain (the daemon exits 78 when computer-use is requested but Accessibility / Screen Recording aren't granted; the OS re-spawns the daemon on the next TCC.db change, so granting consent in System Settings auto-recovers without the user touching launchctl). See COMPUTER_USE.md §Q5.
None require a logged-in GUI session for the WebSocket loop.
Packaging & install
Dimension
macOS
Linux
Windows
Notes
Primary installer
pipx install autogpt-local-executor
pipx install autogpt-local-executor
pipx install autogpt-local-executor
All paths go through pipx in v0.
Secondary
Homebrew tap (brew install autogpt/tap/local-executor)
distro packages (deb/rpm) — best-effort
Scoop / winget — best-effort
Future.
Signing
Apple Developer ID + notarization for .pkg
none (pipx wheels are unsigned)
Authenticode signing for .exe wrapper (SmartScreen)
Required if we ship native installers.
Python runtime
3.11+ via pyenv/pipx-bundled
3.11+ via distro or pyenv
3.11+ via python.org or Microsoft Store
Pin to 3.11 minimum in pyproject.toml.
Keychain / credential storage
Dimension
macOS
Linux
Windows
Notes
Backend
Keychain Services
Secret Service (D-Bus) — gnome-keyring or kwallet
Windows Credential Manager
Via the keyring library.
Headless fallback
n/a (always available)
keyring fails if no D-Bus / no keyring daemon
n/a
Shim detects via keyring.backend.SecretService.priority check; on fail, prompts user to either install gnome-keyring/pass or fall back to an encrypted file at $XDG_STATE_HOME/autogpt-local-executor/tokens.enc with a passphrase derived from a KEYCHAIN_PASSPHRASE env var.
WSL2
n/a
Use the Linux path (D-Bus on WSL2 is rare; encrypted-file fallback is the practical default)
sys.platform == "linux" AND /proc/version lacks microsoft
linux
sys.platform == "linux" AND /proc/version contains microsoft
wsl2
sys.platform == "win32"
windows
platform.machine()
Wire arch value
x86_64, AMD64, amd64
x86_64
arm64, aarch64, ARM64
arm64
anything else
reject HELLO with UNSUPPORTED_ARCH
Path Jail Strategy
The path jail is the most security-critical component of the shim. A naive
prefix string check (path.startswith(allowed_root)) is broken in three
ways:
/Users/alice/autogpt-workspace2 starts with /Users/alice/autogpt-workspace.
On case-insensitive FS, /users/alice/... is the same directory but a
different string.
Symlinks, junctions, and reparse points let an inside-jail path resolve to
outside-jail.
Algorithm
For every FILE_* operation (READ, WRITE, STAT, LIST, DELETE, MOVE — and both
source AND destination of MOVE):
def is_inside_jail(requested_path: str, allowed_root: str) -> bool:
# 1. Lexical normalize. Handle Windows drive letters, UNC, mixed slashes.
p = pathlib.Path(requested_path).expanduser()
# 2. Reject obvious bad shapes BEFORE filesystem access.
if is_reserved_name(p): # Windows: CON, PRN, NUL, COM1, ...
return False
if has_alternate_data_stream(p): # Windows: "file.txt:hidden"
return False
if has_invalid_chars(p): # Windows: < > " | ? * and ctrl chars
return False
# 3. Resolve symlinks, junctions, firmlinks, reparse points.
# Use os.path.realpath, NOT pathlib.Path.resolve() — realpath handles
# junctions correctly on Windows since Python 3.8; resolve() can hang
# on broken symlinks on some FS.
try:
resolved = os.path.realpath(p, strict=False)
except OSError:
return False
root_resolved = os.path.realpath(allowed_root, strict=True)
# 4. Canonical-form comparison.
if is_case_insensitive_fs(root_resolved):
# macOS (APFS default), Windows (NTFS default), Linux on case-folded ext4
a = os.path.normcase(resolved)
b = os.path.normcase(root_resolved)
else:
a = resolved
b = root_resolved
# 5. Strict prefix with path separator boundary.
# Append os.sep to prevent /foo matching /foobar.
if not a.endswith(os.sep):
a_check = a + os.sep
else:
a_check = a
if not b.endswith(os.sep):
b_check = b + os.sep
else:
b_check = b
return a_check.startswith(b_check) or a == root_resolved
def is_case_insensitive_fs(path: str) -> bool:
# Cheapest heuristic: probe the actual filesystem.
# On macOS, default APFS is case-insensitive (but user can format
# case-sensitive APFS). On Windows, NTFS is case-insensitive by default
# (but per-directory case-sensitivity flag exists since Win10 1803).
# On Linux, ext4/btrfs/xfs are case-sensitive (but optional case-folding
# since kernel 5.2).
if sys.platform == "win32":
return True
if sys.platform == "darwin":
# Check the actual volume. Use os.pathconf("/", "PC_CASE_SENSITIVE")
# where available; otherwise probe by creating "X" and looking up "x".
return _probe_case_insensitive(path)
return _probe_case_insensitive(path) # Linux default sensitive, but probe
Key invariants:
Always realpath both sides. Never compare a not-yet-resolved path against
a resolved root.
Always check with a separator boundary./foo must not match /foobar.
Per-volume case-sensitivity probe. Don't hardcode "macOS = insensitive";
modern users can have case-sensitive APFS or per-dir case-sensitive NTFS.
Refuse paths that don't exist when STRICT mode is required. FILE_WRITE
with create_parents: true is the only op that may target a non-existent
path; even then, the parent must exist and be inside the jail.
Windows Subsystem for Linux v2 is a special case: a real Linux kernel running
under a Windows host. The shim runs as a normal Linux process and looks like
Linux to all Python APIs (sys.platform == "linux").
WSL1 also matches this check, but WSL1 is end-of-life and we don't support it.
If we ever need to distinguish, WSL2 has WSL_INTEROP env var set and a
working /proc/sys/kernel/osrelease containing WSL2.
Behavior differences from native Linux
HELLO.platform = "wsl2" (not "linux"). The platform may want to
surface "Running on WSL2" in the UI to set user expectations.
/mnt/c/... paths cross the filesystem boundary. They work, but are
~10× slower than native ext4 paths and use Windows ACLs instead of POSIX
permissions. The shim allows them in allowed_root if the user explicitly
configures one, but the default allowed_root stays inside the Linux home.
No computer use. WSL2 has no display server by default. If the user
has WSLg (Windows 11) running an X server, X11 input injection technically
works, but coordinates won't match what they see on the Windows host. Shim
omits computer_use from capabilities regardless.
Hardware access is limited. USB/serial passthrough requires
usbipd-win on the Windows host. Shim detects /dev/ttyUSB* presence
as usual; absence isn't surprising.
Keychain typically unavailable. Most WSL2 distros don't run a Secret
Service daemon. Encrypted-file fallback (tokens.enc) is the practical
default. Document this in the install guide.
machine_id is per-distro, not per-host. A user with Ubuntu WSL2 and
Debian WSL2 has two separate shims to the platform — that's intentional;
they have separate filesystems.
What the platform should NOT assume about WSL2
Don't assume the user's "real" desktop apps are reachable.
Don't assume /mnt/c/Users/<name> is writable (Windows ACLs may block).
Don't assume 127.0.0.1 from WSL2 reaches the Windows host's services —
it doesn't on WSL2 unless localhostForwarding is configured.
References
PROTOCOL.md — Wire-format enums for platform, arch,
shell, file ops.