Repository navigation
fix(messaging): bind OpenClaw WeChat credentials #10601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 25 commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
420f96b
fix(messaging): bind OpenClaw WeChat credentials
prekshivyas 2294f33
fix(messaging): refresh sealed WeChat credentials
prekshivyas 57a6c47
Merge branch 'main' into codex/fix-10079-openclaw-wechat-binding
prekshivyas c6bcc2a
fix(messaging): detect active WeChat accounts
prekshivyas 1f9b57c
fix(messaging): preflight WeChat placeholder refresh
prekshivyas d0d8334
refactor(messaging): simplify WeChat refresh dispatch
prekshivyas 66429a7
fix(messaging): order WeChat refresh before config writes
prekshivyas 9ac6b97
test(messaging): prove WeChat runtime credential binding
prekshivyas ece29d1
merge(main): refresh PR validation base
prekshivyas 89d5814
docs(messaging): clarify WeChat placeholder invariant
prekshivyas c827152
test(e2e): register WeChat runtime proof helpers
prekshivyas f9918d3
fix(messaging): clear OpenClaw WeChat account state
prekshivyas 0b63a1d
fix(messaging): fail closed on WeChat removal
prekshivyas a6804dc
test(messaging): execute WeChat refresh boundary
prekshivyas 043dd5a
test(e2e): remove WeChat source-shape assertion
prekshivyas 0756a17
fix(messaging): recover failed WeChat removal
prekshivyas 0f3bf90
fix(messaging): verify offline WeChat state mount
prekshivyas c95da08
fix(messaging): isolate offline WeChat volume
prekshivyas 4a8b1d9
merge(main): refresh PR validation base
prekshivyas 38efd25
test(messaging): stage WeChat refresh helper
prekshivyas 633f3a1
merge(main): refresh PR validation base
prekshivyas 66bfee3
fix(messaging): tighten WeChat lifecycle boundaries
prekshivyas 11aeecf
test(messaging): pin WeChat cleanup paths
prekshivyas 6c03dc3
refactor(messaging): centralize WeChat cleanup paths
prekshivyas 60307cd
fix(messaging): diagnose stopped WeChat cleanup
prekshivyas 57a2131
refactor(sandbox): invert channel cleanup dependency
prekshivyas 46ffead
test(e2e): route WeChat fixture changes
prekshivyas 38899ef
refactor(messaging): own WeChat cleanup state in manifest
prekshivyas 9f88d07
merge(main): refresh PR validation base
prekshivyas cfaa17c
chore(runtime): refresh managed startup bundle
prekshivyas 66d8c0f
fix(messaging): gate channel state cleanup by agent
prekshivyas 4412ad9
test(messaging): remove source-shape import assertion
prekshivyas 8595b03
fix(messaging): harden runtime credential refresh
prekshivyas 377778f
merge(main): refresh PR validation base
prekshivyas 0f02e4f
test(runtime): refresh reviewed bundle digest
prekshivyas fd43fdd
test(messaging): use Hermes credential boundary
prekshivyas 398168b
docs(messaging): clarify stopped cleanup condition
prekshivyas 425c003
test(e2e): bind Google Chat fixture at channel boundary
prekshivyas 6adf351
test(e2e): cover Google Chat rebuild provider boundary
prekshivyas 8dfe6f1
test(e2e): bind Google Chat fixture to startup registration
prekshivyas a3da32d
test(e2e): unify Google Chat startup boundary
prekshivyas c41151e
test(e2e): use named Google Chat fixture imports
prekshivyas 6db670e
test(e2e): bind rebuild to Google Chat fixture
prekshivyas c66efc5
test(e2e): keep messaging fixtures behavior-focused
prekshivyas d49ed2a
test(cli): move onboarding import outside timeout
prekshivyas 55f9bd7
merge(main): refresh PR validation base
prekshivyas aeeae73
docs(onboard): clarify policy sync fallback
prekshivyas 47488fa
merge: resolve conflicts with main
github-actions[bot] 07a5b9e
fix(wechat): harden runtime credential cleanup
senthilr-nv 65cbc28
chore(runtime): refresh reviewed startup bundle
senthilr-nv 6dd9805
fix(wechat): clarify cleanup recovery
senthilr-nv 86a7a80
test(runtime): use explicit cleanup image oracle
senthilr-nv 2825fbb
fix(wechat): improve cleanup recovery guidance
senthilr-nv d26ac5e
docs(wechat): explain stopped cleanup recovery
senthilr-nv 653f269
fix(installer): admit wechat preload files
senthilr-nv 9f6cc4c
fix(wechat): roll back partial account refresh
senthilr-nv 58d904d
docs(wechat): qualify stopped cleanup fallback
senthilr-nv c3b5f1c
fix(wechat): narrow refresh abort handling
senthilr-nv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| #!/usr/bin/env python3 | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import json | ||
| import os | ||
| import re | ||
| import secrets | ||
| import stat | ||
| import sys | ||
|
|
||
| config_file = os.path.abspath(sys.argv[1]) | ||
| openclaw_dir = os.path.dirname(config_file) | ||
| env_key = "WECHAT_BOT_TOKEN" | ||
| canonical = f"openshell:resolve:env:{env_key}" | ||
| scoped_re = re.compile(rf"^openshell:resolve:env:v[0-9]+_{env_key}$") | ||
|
|
||
|
|
||
| def fail(message): | ||
| print(f"[SECURITY] Refusing WeChat provider placeholder refresh — {message}", file=sys.stderr) | ||
| raise SystemExit(1) | ||
|
|
||
|
|
||
| def safe_account_id(value): | ||
| return ( | ||
| isinstance(value, str) | ||
| and value | ||
| and value == value.strip() | ||
| and value not in {".", ".."} | ||
| and ".." not in value | ||
| and "/" not in value | ||
| and "\\" not in value | ||
| and not any(ord(char) < 32 or ord(char) == 127 for char in value) | ||
| ) | ||
|
|
||
|
|
||
| if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"): | ||
| fail("the platform cannot enforce no-follow account traversal") | ||
|
|
||
| close_on_exec = getattr(os, "O_CLOEXEC", 0) | ||
| directory_flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | close_on_exec | ||
| file_flags = os.O_RDONLY | os.O_NOFOLLOW | close_on_exec | ||
|
|
||
| descriptors = [] | ||
| try: | ||
| try: | ||
| root_fd = os.open(openclaw_dir, directory_flags) | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| descriptors.append(root_fd) | ||
| config_fd = os.open("openclaw.json", file_flags, dir_fd=root_fd) | ||
| try: | ||
| config_metadata = os.fstat(config_fd) | ||
| if not stat.S_ISREG(config_metadata.st_mode) or config_metadata.st_nlink != 1: | ||
| fail("openclaw.json is not a single regular file") | ||
| with os.fdopen(config_fd, "r", encoding="utf-8") as stream: | ||
| config_fd = -1 | ||
| config = json.load(stream) | ||
| finally: | ||
| if config_fd >= 0: | ||
| os.close(config_fd) | ||
| except (OSError, ValueError, json.JSONDecodeError): | ||
| fail("openclaw.json is unreadable or unsafe") | ||
|
|
||
| channels = config.get("channels") if isinstance(config, dict) else None | ||
| channel = channels.get("openclaw-weixin") if isinstance(channels, dict) else None | ||
| if not isinstance(channel, dict) or channel.get("enabled") is False: | ||
| raise SystemExit(0) | ||
|
|
||
| accounts = channel.get("accounts") | ||
| if not isinstance(accounts, dict): | ||
| raise SystemExit(0) | ||
|
|
||
| account_ids = [] | ||
| for account_id, account in accounts.items(): | ||
| if not isinstance(account, dict) or account.get("enabled") is False: | ||
| continue | ||
| if not safe_account_id(account_id): | ||
| fail("active WeChat configuration contains an unsafe account id") | ||
| account_ids.append(account_id) | ||
|
|
||
| if not account_ids: | ||
| raise SystemExit(0) | ||
|
|
||
| runtime_placeholder = os.environ.get(env_key, "") | ||
| if not scoped_re.fullmatch(runtime_placeholder): | ||
| if not runtime_placeholder: | ||
| fail(f"{env_key} is missing from the runtime environment") | ||
| if not runtime_placeholder.startswith("openshell:resolve:env:"): | ||
| fail(f"{env_key} is not an OpenShell placeholder; raw credentials stay out of account files") | ||
| fail(f"{env_key} is not the required revision-scoped OpenShell placeholder") | ||
|
|
||
| try: | ||
| plugin_fd = os.open("openclaw-weixin", directory_flags, dir_fd=root_fd) | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| descriptors.append(plugin_fd) | ||
| accounts_fd = os.open("accounts", directory_flags, dir_fd=plugin_fd) | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| descriptors.append(accounts_fd) | ||
| except OSError: | ||
| fail("the managed account directory is missing or unsafe") | ||
|
|
||
| pending = [] | ||
| for account_id in sorted(account_ids): | ||
| filename = f"{account_id}.json" | ||
| try: | ||
| account_fd = os.open(filename, file_flags, dir_fd=accounts_fd) | ||
| except OSError: | ||
| fail("a managed account file is missing or unsafe") | ||
| try: | ||
| metadata = os.fstat(account_fd) | ||
| if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: | ||
| fail("a managed account file is not a single regular file") | ||
| if stat.S_IMODE(metadata.st_mode) & 0o077: | ||
|
senthilr-nv marked this conversation as resolved.
Outdated
|
||
| fail("a managed account file is accessible outside its owner") | ||
| try: | ||
| with os.fdopen(os.dup(account_fd), "r", encoding="utf-8") as stream: | ||
| account_data = json.load(stream) | ||
| except Exception: | ||
| fail("a managed account file is unreadable") | ||
| finally: | ||
| os.close(account_fd) | ||
|
|
||
| if not isinstance(account_data, dict) or not isinstance(account_data.get("token"), str): | ||
| fail("a managed account file has no valid token field") | ||
| current = account_data["token"] | ||
| if current == runtime_placeholder: | ||
| continue | ||
| if current != canonical and not scoped_re.fullmatch(current): | ||
| fail("a managed account token is neither canonical nor revision-scoped") | ||
| account_data["token"] = runtime_placeholder | ||
| payload = (json.dumps(account_data, ensure_ascii=False, indent=2) + "\n").encode("utf-8") | ||
| pending.append((filename, metadata, payload)) | ||
|
|
||
| for filename, metadata, payload in pending: | ||
| temporary = f".{filename}.nemoclaw-{os.getpid()}-{secrets.token_hex(8)}.tmp" | ||
| temporary_created = False | ||
| try: | ||
| create_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | ||
| temporary_fd = os.open(temporary, create_flags, 0o600, dir_fd=accounts_fd) | ||
| temporary_created = True | ||
| try: | ||
| os.fchmod(temporary_fd, 0o600) | ||
| if os.geteuid() == 0: | ||
| os.fchown(temporary_fd, metadata.st_uid, metadata.st_gid) | ||
| offset = 0 | ||
| while offset < len(payload): | ||
| offset += os.write(temporary_fd, payload[offset:]) | ||
| os.fsync(temporary_fd) | ||
| finally: | ||
| os.close(temporary_fd) | ||
|
|
||
| current_metadata = os.stat(filename, dir_fd=accounts_fd, follow_symlinks=False) | ||
| before = (metadata.st_dev, metadata.st_ino, metadata.st_mtime_ns, metadata.st_size) | ||
| after = ( | ||
| current_metadata.st_dev, | ||
| current_metadata.st_ino, | ||
| current_metadata.st_mtime_ns, | ||
| current_metadata.st_size, | ||
| ) | ||
| if before != after or current_metadata.st_nlink != 1: | ||
| fail("a managed account file changed during refresh") | ||
| os.replace( | ||
| temporary, | ||
| filename, | ||
| src_dir_fd=accounts_fd, | ||
| dst_dir_fd=accounts_fd, | ||
| ) | ||
| temporary_created = False | ||
| os.fsync(accounts_fd) | ||
| finally: | ||
| if temporary_created: | ||
| try: | ||
| os.unlink(temporary, dir_fd=accounts_fd) | ||
| except OSError: | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| pass | ||
|
|
||
| if pending: | ||
| print( | ||
| f"[config] Refreshed WeChat account provider placeholder from OpenShell runtime env: {env_key}", | ||
| file=sys.stderr, | ||
| ) | ||
| finally: | ||
| for descriptor in reversed(descriptors): | ||
| os.close(descriptor) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.