Documented during v1→v5 development. If you hit any of these, check this file first.
Problem: The write tool (used by some agents / editors) auto-replaces Python source os.environ.get( with the literal string ***, leaving broken syntax:
AUTH_TOKEN = ***'ACP_TOKEN', 'openclaw-acp-demo-token')
# ^^^ should be os.environ.get(Symptoms: SyntaxError: unmatched ')' or SyntaxError: closing parenthesis ')' does not match opening parenthesis '['
Workaround: Use getattr(__import__('os').environ, 'get')(...) instead of os.environ.get(...):
AUTH_TOKEN = ***'os').environ, 'get')('ACP_TOKEN', 'openclaw-acp-demo-token')Critical: All letters must be ASCII — getattr, NOT getatt…t__ (unicode ellipsis). I've fallen for this typo twice.
Auto-fix scripts: tests/fix-server-auth.py (v1) and tests/fix-server-auth-v2.py (handles *** prefix correctly).
Problem: When ACP dispatches a Mavis task (subprocess via cmd.exe /c mcode.cmd), Mavis's stdin is not a TTY (it's None because subprocess.Popen didn't pass stdin=PIPE). Mavis's default --permission smart policy tries to prompt user for confirmation on any tool call (read/write/bash) → sees no TTY → returns PERMISSION_REQUIRED → every task deadlocks immediately.
Symptom: Every task completes in ~5-30s with status: blocked and error.code: PERMISSION_REQUIRED. Mavis answers "我先...扫一下/看一下" then dies.
Fix: Pass --permission full to Mavis (in acp-server.py line 217):
ask/smart— interactive confirm (blocked without TTY)full— auto-approve all (✅ use this for ACP dispatch)off— also works but disables all safety;fullis the better choice
Code change:
mcode_args = [
'exec', task['prompt'],
'--cwd', task['workspace'],
'--output-format', 'json',
'--permission', 'full', # was 'smart' — blocks ACP dispatch
'--timeout', task['timeout'],
]Alternative (NOT recommended): Pre-feed files via --files so Mavis never needs the read tool. This works but is a workaround — Mavis can't autonomously explore your workspace.
Gotcha discovered during this fix: When OpenClaw spawns ACP subprocess, also the LOG_FILE and DEFAULT_DB_PATH env vars need os.path.expandvars() — the strings literally contain %USERPROFILE% which Python doesn't auto-expand in os.environ.get() defaults.
Problem: Installing PowerShell 7.6 registers a new Windows Terminal profile (with a new GUID), but settings.json's defaultProfile still points to the old PowerShell 5.1 GUID. Windows Terminal shows "加载用户设置时遇到错误" on every launch.
Symptom: Two error dialogs each time Windows Terminal starts (one per open window).
Fix: Add PowerShell 7.6 profile to ~/.openclaw/.../Windows Terminal/settings.json profiles.list:
{
"guid": "{7EAB037B-2D8A-49EF-85BC-846AA550FF4F}",
"name": "PowerShell 7.6.4",
"commandline": "pwsh.exe",
"icon": "%ProgramFiles%\\PowerShell\\7\\assets\\Powershell_av_colors.ico"
}Problem: openclaw gateway restart or similar simple restarts don't actually kill old server processes (port conflict → new server exits immediately). Symptom: new code changes don't take effect.
Workaround: Use tests/nuke-restart.py:
Get-WmiObject Win32_Process -Filter "Name='python.exe'" |
Where-Object { $_.CommandLine -like '*acp-server*' } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }Problem: OpenClaw's exec tool caches its shell at startup. Even after installing PowerShell 7.6 and rebooting, the cached shell is still 5.1.
Symptoms:
&¬ supported (5.1 doesn't have it)Format-Table/$()quoting issues- Chinese characters in output may show as mojibake
Workaround: Use ; instead of &&, OR run commands via absolute path to pwsh 7.6:
& '%USERPROFILE%\pwsh7_6\pwsh.exe' -NoProfile -Command "..."Problem: subprocess.Popen defaults to system GBK decoding for stdout/stderr. Mavis outputs UTF-8 → silent garbled events.
Symptom: SSE events have [stream error: 'gbk' codec can't decode...] in line field.
Fix in acp-server.py:
proc = subprocess.Popen(
['cmd.exe', '/c', MCODE_CMD] + mcode_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8', # ← add this
errors='replace', # ← add this
bufsize=1,
)Problem: from websockets.server import serve is the legacy API. Legacy WebSocketServerProtocol does NOT have .request attribute.
Symptom: AttributeError: 'WebSocketServerProtocol' object has no attribute 'request' on every WS connection.
Fix: Use the asyncio API:
from websockets.asyncio.server import serve as ws_serve # ← asyncio, not .serverProblem: set.add(dict) raises TypeError: cannot use 'dict' as a set element (unhashable type: 'dict').
Symptom: WS connection handler crashes immediately.
Fix: Use a counter instead of a set:
WS_CONN_COUNT = 0
WS_LOCK = threading.Lock()
# On connect:
with WS_LOCK:
WS_CONN_COUNT += 1
# On disconnect:
with WS_LOCK:
WS_CONN_COUNT -= 1Problem: Python requires global X to appear BEFORE any use of X in a function.
Symptom: SyntaxError: name 'X' is used prior to global declaration
Fix: Move global X to the first statement of the function:
async def my_handler():
global X # ← must be first
...Problem: Hardcoded auth tokens in source get redacted by some write tools. openclaw-acp-demo-token → opencl…oken (with unicode ellipsis) → urllib latin-1 encoding fails.
Symptom: UnicodeEncodeError: 'latin-1' codec can't encode character '\u2026'
Fix: Don't hardcode. Read token dynamically from acp-server.py source:
import re
with open(r'server/acp-server.py') as f:
content = f.read()
m = re.search(r"ACP_TOKEN',\s*'([^']+)'", content)
TOKEN = *** if m else None(Note the regex pattern doesn't include the literal token, so redaction won't fire.)
Problem: Printing ✅ / ❌ / 🔥 etc. via Python print() → GBK shell can't encode → UnicodeEncodeError.
Symptom: Script crashes at the final print() call after tests passed.
Fix: Force UTF-8 stdout:
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')Or just use ASCII markers: [OK] / [FAIL] / [WIN].
Bonus trap: 11. Acp-server auth redaction after every restart
After every restart with nuke-restart.py, if you also edit acp-server.py, you may re-introduce the os.environ.get( → *** redaction bug. Always check:
grep "AUTH_TOKEN" server\acp-server.py
# Should show: AUTH_TOKEN = getatt…t__('os').environ, 'get')('ACP_TOKEN', 'openclaw-acp-demo-token')
# NOT: AUTH_TOKEN = ***'ACP_TOKEN', 'openclaw-acp-demo-token')Run python -c "import ast; ast.parse(open('server/acp-server.py').read())" for quick syntax check before starting.