refactor(doctor): extract provider and shared config helpers#51753
refactor(doctor): extract provider and shared config helpers#51753vincentkoc merged 12 commits intomainfrom
Conversation
Greptile SummaryThis PR extracts provider-specific doctor helpers (Telegram, Discord) and shared config-repair utilities (allowlist, open-policy, mutable-allowlist, empty-allowlist policy) out of the monolithic Key observations:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/commands/doctor/shared/empty-allowlist-policy.ts
Line: 1
Comment:
**Layering violation: `shared/` depends on `providers/`**
`shared/empty-allowlist-policy.ts` imports `collectTelegramGroupPolicyWarnings` from `../providers/telegram.js`. This inverts the intended dependency direction: shared utilities should not depend on provider-specific modules. If a future provider is added and also needs custom group-policy warning logic, this shared file would need to be reopened and extended with another provider-specific branch — exactly the tight coupling this refactor was meant to eliminate.
Consider either:
- Moving the Telegram-specific branch out of this shared function and into the Telegram provider layer (calling `collectTelegramGroupPolicyWarnings` at the call site in `doctor-config-flow.ts` before/after `collectEmptyAllowlistPolicyWarningsForAccount`), or
- Accepting a callbacks/strategy pattern here so the shared helper stays provider-agnostic.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/commands/doctor/shared/open-policy-allowfrom.ts
Line: 4-14
Comment:
**Duplicated `resolveAllowFromMode` across two shared files**
`resolveAllowFromMode` (and its accompanying mode type) is defined identically in both `open-policy-allowfrom.ts` (as `OpenPolicyAllowFromMode`) and `allowlist-policy-repair.ts` (as `AllowFromMode`). The logic is byte-for-byte identical in both places. If the channel-to-mode mapping ever needs to change, it now has to be updated in two places.
Consider extracting it to `src/commands/doctor/shared/allow-from-mode.ts` (or similar) and importing it from both files.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "refactor(doctor): ex..." |
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🔴 SSRF / Telegram bot token exfiltration via configurable apiRoot/proxyUrl during
|
| Property | Value |
|---|---|
| Severity | Critical |
| CWE | CWE-918 |
| Location | src/commands/doctor/providers/telegram.ts:209-216 |
Description
The doctor --fix flow attempts to auto-resolve non-numeric Telegram allowFrom entries (e.g. @username) by calling lookupTelegramChatId() with:
token: resolved Telegram bot tokenapiRoot: configurable URL (channels.telegram.apiRoot/ per-account override)proxyUrl: configurable proxy URL (channels.telegram.proxy/ per-account override)
Because apiRoot is not allowlisted to Telegram-owned hosts and can be any URL, running doctor --fix on an attacker-influenced config can:
- Trigger SSRF to arbitrary hosts (including internal network endpoints) from the machine running the CLI
- Exfiltrate the bot token because the Telegram Bot API token is embedded in the request path (
/bot<token>/...) insidelookupTelegramChatId().
This is especially risky in CI or other automation where secrets are present but configs may come from untrusted branches/PRs.
Vulnerable call site:
const id = await lookupTelegramChatId({
token: account.token,
chatId: username,
apiRoot: account.apiRoot,
proxyUrl: account.proxyUrl,
network: account.network,
});Downstream sink (token in URL path) occurs inside extensions/telegram/src/api-fetch.ts where the URL is constructed as ${apiBase}/bot${token}/getChat?....
Recommendation
Restrict where doctor --fix can send Telegram bot tokens.
Safer options (pick one):
- Ignore custom endpoints for doctor repairs (recommended for
doctor --fix):
const id = await lookupTelegramChatId({
token: account.token,
chatId: username,
signal: controller.signal,
// Do NOT forward apiRoot/proxyUrl from config in this auto-repair path
});- Allowlist Telegram API hosts + require HTTPS before forwarding
apiRoot:
function isSafeTelegramApiRoot(apiRoot?: string): boolean {
if (!apiRoot) return true;
const u = new URL(apiRoot);
return u.protocol === "https:" && (u.hostname === "api.telegram.org");
}
const safeApiRoot = isSafeTelegramApiRoot(account.apiRoot) ? account.apiRoot : undefined;
const id = await lookupTelegramChatId({ token: account.token, chatId: username, apiRoot: safeApiRoot });- If custom endpoints/proxies are needed, require an explicit CLI opt-in (e.g.
--allow-custom-telegram-endpoints) and print a warning explaining that the bot token will be sent to that endpoint.
2. 🔵 Terminal/log injection via unsanitized config values in doctor output
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-117 |
| Location | src/commands/doctor-config-flow.ts:990-1015 |
Description
The doctor command renders user-controlled config values directly into terminal output without output neutralization.
- Values derived from configuration (e.g., allowlist entries, IDs/keys embedded in config paths) are interpolated into strings.
- These strings are printed via
note(...)(Clack terminal UI) without sanitizing ANSI escape sequences or control characters. - A malicious config value containing newlines or ANSI/OSC sequences can forge/spoof log lines, hide warnings, or manipulate terminal display in CI/local terminals.
Vulnerable code (examples):
const exampleLines = mutableAllowlistHits
.slice(0, 8)
.map((hit) => `- ${hit.path}: ${hit.entry}`)
.join("\n");
...
note(
[
`- Found ${mutableAllowlistHits.length} mutable allowlist ...`,
exampleLines,
...
].join("\n"),
"Doctor warnings",
);Related call sites introduced/expanded in this change also build output lines from raw config entries, e.g.:
-
src/commands/doctor/providers/telegram.ts:changes.push(\- ${pathLabel}: resolved ${rep.from} -> ${rep.to}`)whererep.from` comes from config allowFrom entries.
Note: the repo already includes a helper that would mitigate this (sanitizeForLog in src/terminal/ansi.ts), but these interpolations do not use it.
Recommendation
Sanitize/neutralize untrusted values before interpolating them into terminal/log output.
Option A (preferred): sanitize only the untrusted parts (paths/entries coming from config):
import { sanitizeForLog } from "../terminal/ansi.js";
const exampleLines = mutableAllowlistHits
.slice(0, 8)
.map((hit) => `- ${sanitizeForLog(hit.path)}: ${sanitizeForLog(hit.entry)}`)
.join("\n");Option B: sanitize the complete message right before printing (be careful if you intentionally include ANSI formatting):
note(sanitizeForLog(message), "Doctor warnings");Also apply the same sanitization to other doctor-generated change/warning lines that include config-derived values (e.g., Telegram rep.from, dynamic prefix/object keys).
Analyzed PR: #51753 at commit b02f620
Last updated on: 2026-03-21T17:40:50Z
* main: (516 commits) fix: use content hash for memory flush dedup instead of compactionCount (openclaw#30115) (openclaw#34222) fix(tts): add matrix to VOICE_BUBBLE_CHANNELS (openclaw#37080) feat(memory): pluggable system prompt section for memory plugins (openclaw#40126) fix: detect nvm services from installed command (openclaw#51146) fix: handle Linux nvm CA env before startup (openclaw#51146) (thanks @GodsBoy) refactor: route Telegram runtime through plugin sdk (openclaw#51772) refactor: route iMessage runtime through plugin sdk (openclaw#51770) refactor: route Slack runtime through plugin sdk (openclaw#51766) refactor(doctor): extract provider and shared config helpers (openclaw#51753) Fix Discord `/codex_resume` picker expiration (openclaw#51260) fix(ci): remove duplicate embedding default export fix(ci): restore embedding defaults and plugin boundaries fix: compaction safeguard summary budget (openclaw#27727) web UI: fix context notice using accumulated inputTokens instead of prompt snapshot (openclaw#51721) fix(status): skip cold-start status probes refactor(doctor): extract telegram provider warnings (openclaw#51704) fix(telegram): default fresh setups to mention-gated groups docs(changelog): note telegram doctor first-run guidance fix(doctor): add telegram first-run guidance fix(doctor): suppress telegram fresh-install group warning ...
…w#51753) * refactor(doctor): add shared doctor types * refactor(doctor): add shared allowlist helpers * refactor(doctor): extract empty allowlist warnings * refactor(doctor): extract telegram allowfrom scanning * refactor(doctor): extract telegram allowfrom repair * refactor(doctor): extract discord id repair * refactor(doctor): add shared object helpers * refactor(doctor): extract mutable allowlist scanning * refactor(doctor): extract open-policy allowfrom repair * refactor(doctor): extract allowlist policy repair * fix(doctor): unblock discord provider refactor checks * refactor(doctor): fix provider layering in shared warnings
…w#51753) * refactor(doctor): add shared doctor types * refactor(doctor): add shared allowlist helpers * refactor(doctor): extract empty allowlist warnings * refactor(doctor): extract telegram allowfrom scanning * refactor(doctor): extract telegram allowfrom repair * refactor(doctor): extract discord id repair * refactor(doctor): add shared object helpers * refactor(doctor): extract mutable allowlist scanning * refactor(doctor): extract open-policy allowfrom repair * refactor(doctor): extract allowlist policy repair * fix(doctor): unblock discord provider refactor checks * refactor(doctor): fix provider layering in shared warnings
…w#51753) * refactor(doctor): add shared doctor types * refactor(doctor): add shared allowlist helpers * refactor(doctor): extract empty allowlist warnings * refactor(doctor): extract telegram allowfrom scanning * refactor(doctor): extract telegram allowfrom repair * refactor(doctor): extract discord id repair * refactor(doctor): add shared object helpers * refactor(doctor): extract mutable allowlist scanning * refactor(doctor): extract open-policy allowfrom repair * refactor(doctor): extract allowlist policy repair * fix(doctor): unblock discord provider refactor checks * refactor(doctor): fix provider layering in shared warnings
The doctor config-flow refactor series (PRs openclaw#51753–openclaw#52067) extracted shared modules into src/commands/doctor/shared/ with companion test files. default-account-warnings.ts was the only substantive module (157 lines, two public functions) without direct test coverage. Adds 16 tests covering both exported functions: - collectMissingDefaultAccountBindingWarnings: wildcard bindings, partial coverage, no bindings, multi-channel isolation - collectMissingExplicitDefaultAccountWarnings: single vs multi account, matching/mismatched defaultAccount, missing defaultAccount Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…w#51753) * refactor(doctor): add shared doctor types * refactor(doctor): add shared allowlist helpers * refactor(doctor): extract empty allowlist warnings * refactor(doctor): extract telegram allowfrom scanning * refactor(doctor): extract telegram allowfrom repair * refactor(doctor): extract discord id repair * refactor(doctor): add shared object helpers * refactor(doctor): extract mutable allowlist scanning * refactor(doctor): extract open-policy allowfrom repair * refactor(doctor): extract allowlist policy repair * fix(doctor): unblock discord provider refactor checks * refactor(doctor): fix provider layering in shared warnings
Summary
src/commands/doctor/providers/*src/commands/doctor/shared/*src/commands/doctor-config-flow.tstoward orchestration instead of inline scanning/repair logicWhy
This is maintainer-requested refactor work to make the doctor command easier to extend channel-by-channel without reopening one large flow file for every warning or repair path.
Included in this phase
Validation
pnpm exec vitest run src/commands/doctor/providers/telegram.test.ts src/commands/doctor/providers/discord.test.ts src/commands/doctor/shared/allowlist.test.ts src/commands/doctor/shared/empty-allowlist-policy.test.ts src/commands/doctor/shared/mutable-allowlist.test.ts src/commands/doctor/shared/open-policy-allowfrom.test.ts src/commands/doctor-config-flow.test.ts7 passed,52 passedNotes
codex review --base origin/mainwas attempted locally, but the localpaperMCP transport failed during startup (127.0.0.1:80connection refused), so no clean Codex review result was available from this machine.