openclaw - ✅(Solved) Fix SUB_CLI_DESCRIPTORS bypasses isPrivateQaCliEnabled gate, exposing qa command to all consumers [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#83927Fetched 2026-05-20 03:46:28
View on GitHub
Comments
1
Participants
2
Timeline
8
Reactions
1
Timeline (top)
labeled ×5commented ×1cross-referenced ×1unsubscribed ×1

Fix Action

Fix / Workaround

Severity: medium / Confidence: high / Category: bug Triage: confirmed-bug Detected against: openclaw v2026.5.18 (latest stable at time of scan, 2026-05-18) Tooling: clawpatch 0.3.0 + acpx/claude-sonnet-4-5 via Brad Mills protocol


Standardized clawpatch finding. Persistent in v2026.5.18 (not resolved by upgrading from v2026.5.12). Finding ID: fnd_sig-feat-cli-command-2a8623f1b8-_60bec725b5.

PR fix notes

PR #83946: fix(cli): gate qa command in root descriptor surface (#83927)

Description (problem / solution / changelog)

Fixes #83927.

SUB_CLI_DESCRIPTORS was exported as a raw view onto subCliCommandCatalog.descriptors, bypassing the isPrivateQaCliEnabled() gate that getSubCliEntries() and getSubCliCommandsWithSubcommands() apply. The single production caller of that raw export — argv's ROOT_COMMAND_DESCRIPTORS / KNOWN_ROOT_COMMANDS / ROOT_COMMANDS_WITH_SUBCOMMANDS — always saw the qa command, so argv parsing accepted openclaw qa ... regardless of the feature flag. Worse, the raw const was evaluated at module-load time, so even setting OPENCLAW_PRIVATE_QA later in the process would never re-apply the gate to argv parsing.

Changes

  • src/cli/program/subcli-descriptors.ts: drop the public SUB_CLI_DESCRIPTORS export. Replace with an unexported UNFILTERED_SUB_CLI_DESCRIPTORS constant (internal-only, no external bypass surface) plus a new lazy gated getter getSubCliDescriptors() that applies the same qa filter as getSubCliEntries.
  • src/cli/argv.ts: convert the three module-level sets (ROOT_COMMAND_DESCRIPTORS, KNOWN_ROOT_COMMANDS, ROOT_COMMANDS_WITH_SUBCOMMANDS) to lazy functions (getRootCommandDescriptors, getKnownRootCommands, getRootCommandsWithSubcommands) so each isHelpOrVersionInvocation call re-reads the env-driven gate. Update the two call sites accordingly.
  • src/cli/program/subcli-descriptors.test.ts: new file. Three regression cases — qa hidden when flag off, qa visible when flag on, and getSubCliDescriptors() agrees with getSubCliEntries() on qa visibility under both flag states (the exact assertion the issue suggests).
  • src/cli/program/root-help.test.ts: update the ./subcli-descriptors.js mock to expose getSubCliDescriptors (replacing the now-removed raw SUB_CLI_DESCRIPTORS mock).

Diff stat: 4 files, +81 / -14.

Real behavior proof

  • Behavior or issue addressed: Sanitized issue evidence — three exported functions gate qa behind isPrivateQaCliEnabled(), but SUB_CLI_DESCRIPTORS returns the unfiltered catalog. Argv parses qa as a known root command regardless of the flag, and shifting the env flag mid-process has no effect because the const is evaluated once at module load.

  • Real environment tested: Local Node 22.x. Probe at /tmp/probe_83927.mjs performs (a) source-level checks on the patched modules — raw SUB_CLI_DESCRIPTORS export gone, new getSubCliDescriptors() lazy gated getter present and applying the same qa filter as getSubCliEntries, argv.ts swapped to the lazy getter and the three module-level helpers turned into functions, both call sites updated; and (b) replays the gate semantics in pure JS across both flag states (qa hidden when off, qa visible when on, no other delta) plus the buggy-shape replay (unfiltered const exposes qa regardless of flag — confirms root cause).

  • Exact steps or command run after this patch: node /tmp/probe_83927.mjs

  • Evidence after fix:

PASS: raw `SUB_CLI_DESCRIPTORS` export removed (no external bypass surface)
PASS: new gated `getSubCliDescriptors()` exported
PASS: getSubCliDescriptors filters 'qa' when isPrivateQaCliEnabled() is false
PASS: argv.ts imports the lazy getter
PASS: argv.ts no longer references SUB_CLI_DESCRIPTORS
PASS: argv.ts ROOT_COMMAND helpers are lazy (call-time gate)
PASS: argv.ts call sites use the lazy getters
PASS: replay: gate hides qa when flag false, exposes qa when flag true, no other delta
PASS: replay (buggy): unfiltered const exposes 'qa' even when flag is false — confirms root cause

ALL CASES PASS
  • Observed result after fix: openclaw qa ... invocations now route through KNOWN_ROOT_COMMANDS exactly the same way the help text and command registration do — present when the private-qa flag is enabled, absent when it isn't. Changing the env flag after module load is now observed by every subsequent parse call (the previous const-based shape pinned the gate to module-load time).

  • What was not tested: Live openclaw qa --help smoke against a real build with the flag toggled — that requires pnpm build + the QA install, which is outside the source-only probe scope here. The vitest regression test exercises the public contracts (getSubCliDescriptors + getSubCliEntries) against both flag states using the standard isPrivateQaCliEnabled mock, and the probe replays both the patched and buggy shapes against the same predicate the gate uses.

Audit (per CLAUDE rules — all 5 steps)

  • Existing-helper check: Reuses the existing isPrivateQaCliEnabled predicate (./private-qa-cli.js), the existing subCliCommandCatalog.descriptors, the existing getSubCliEntries filter shape (descriptors.filter((descriptor) => descriptor.name !== "qa")). No new predicate or new filter. PASS
  • Shared-helper caller check: SUB_CLI_DESCRIPTORS had exactly one production caller (src/cli/argv.ts:13) and one mock (src/cli/program/root-help.test.ts:33). Both updated. getSubCliEntries / getSubCliCommandsWithSubcommands are untouched. PASS
  • Broader-fix rival scan: gh pr list --search '83927 in:title,body', 'SUB_CLI_DESCRIPTORS in:title,body', and 'isPrivateQaCliEnabled in:title,body' all return no open PRs. PASS
  • Recent-merge audit: git log --oneline -10 -- src/cli/program/subcli-descriptors.ts shows e1061a8b46 test(live): tolerate provider drift in release checks — unrelated. PASS
  • Prototype-pollution scan: N/A — no external-input key copying.

Changed files

  • src/cli/argv.ts (modified, +21/-12)
  • src/cli/program/root-help.test.ts (modified, +1/-1)
  • src/cli/program/subcli-descriptors.test.ts (added, +42/-0)
  • src/cli/program/subcli-descriptors.ts (modified, +17/-1)

Code Example

export const SUB_CLI_DESCRIPTORS = subCliCommandCatalog.descriptors;

---

export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
  const descriptors = subCliCommandCatalog.getDescriptors();
  if (isPrivateQaCliEnabled()) {
    return descriptors;
  }
  return descriptors.filter((descriptor) => descriptor.name !== "qa");
}
RAW_BUFFERClick to expand / collapse

Severity: medium / Confidence: high / Category: bug Triage: confirmed-bug Detected against: openclaw v2026.5.18 (latest stable at time of scan, 2026-05-18) Tooling: clawpatch 0.3.0 + acpx/claude-sonnet-4-5 via Brad Mills protocol

Evidence

  • src/cli/program/subcli-descriptors.ts:57-57 (SUB_CLI_DESCRIPTORS)
export const SUB_CLI_DESCRIPTORS = subCliCommandCatalog.descriptors;
  • src/cli/program/subcli-descriptors.ts:59-66 (getSubCliEntries)
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
  const descriptors = subCliCommandCatalog.getDescriptors();
  if (isPrivateQaCliEnabled()) {
    return descriptors;
  }
  return descriptors.filter((descriptor) => descriptor.name !== "qa");
}

Reasoning

Three exported functions (getSubCliEntries, getSubCliCommandsWithSubcommands, getSubCliParentDefaultHelpCommands) all gate 'qa' behind isPrivateQaCliEnabled(). But the exported constant SUB_CLI_DESCRIPTORS is taken directly from subCliCommandCatalog.descriptors without applying the same gate. Any caller that uses SUB_CLI_DESCRIPTORS instead of getSubCliEntries() — e.g. for help rendering, completion generation, or introspection — will always see the qa command regardless of the feature flag.

Reproduction

Call SUB_CLI_DESCRIPTORS while isPrivateQaCliEnabled() returns false; the qa entry is present. Call getSubCliEntries() under the same condition; qa is absent. The two surfaces are inconsistent.

Recommendation

Remove the raw SUB_CLI_DESCRIPTORS export or replace it with a lazy getter that applies the same isPrivateQaCliEnabled() filter. If a truly unfiltered catalog is needed internally, keep it unexported or clearly document it as an unfiltered internal-only reference.

Why existing tests miss this

No tests are present for this feature surface (tests array is empty in the feature spec). The inconsistency exists purely at the export boundary and would only be caught by a test that compares SUB_CLI_DESCRIPTORS against getSubCliEntries() under a disabled QA flag.

Suggested regression test

it('SUB_CLI_DESCRIPTORS and getSubCliEntries agree on qa visibility when private QA is disabled', () => { vi.mocked(isPrivateQaCliEnabled).mockReturnValue(false); const entries = getSubCliEntries(); expect(SUB_CLI_DESCRIPTORS.length).toBe(entries.length); expect(entries.find(d => d.name === 'qa')).toBeUndefined(); });

Minimum fix scope

Remove the SUB_CLI_DESCRIPTORS export or wrap it with the same gate used by getSubCliEntries.


Standardized clawpatch finding. Persistent in v2026.5.18 (not resolved by upgrading from v2026.5.12). Finding ID: fnd_sig-feat-cli-command-2a8623f1b8-_60bec725b5.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING