fix(control-ui): config form save corrupts 64-bit id strings in string|number fields (#126402)

* fix(control-ui): stop config form save from corrupting 64-bit id strings

Saving the schema-driven config form coerced every numeric-looking string
to a JS number before submission. For union-typed fields such as
tools.elevated.allowFrom.* (anyOf: string | number), string entries
holding 64-bit ids (Discord/Telegram snowflakes) were rewritten through
Number(), which rounds past 2^53:
"1048113311314608148" -> 1048113311314608100. The corruption also hit
untouched fields, because serialization coerces the whole form, so merely
saving an unrelated setting silently broke elevated-approval allowlists
(fail-closed: the real user id no longer matched).

Two guards fix this:
- coerceFormValues keeps a string that already satisfies a string variant
  of an anyOf/oneOf union instead of parsing it into another variant's
  number.
- coerceConfigFormNumberString refuses lossy integer parses: plain
  integer text beyond Number.MAX_SAFE_INTEGER that does not round-trip
  through BigInt stays a string, so pure number/integer fields fail
  validation loudly instead of storing a corrupted id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(control-ui): harden 64-bit config id preservation

* fix(control-ui): validate mixed-union scalar branches

* test(control-ui): prove real gateway id preservation

* test(control-ui): use communications route for config proof

* test(control-ui): grant config proof admin scope

* test(control-ui): reopen raw config for proof

* fix(control-ui): preserve explicit union input types

* test(control-ui): exercise union collection draft

* ci: retry flaky control ui e2e

* fix(control-ui): preserve mixed scalar branch types

* ci: retry service worker e2e

* fix(control-ui): preserve typeless string union branches

* fix(control-ui): reject lossy decimal coercion

* fix(control-ui): reject lossy pure numeric input

* fix(control-ui): preserve exact numeric branch semantics

* ci: retry checkout rate limit

* ci(control-ui): capture real gateway proof

* test(control-ui): frame config proof values

* ci: retry checkout download

* test(control-ui): prove Gateway-served production bundle

* fix(control-ui): preserve exact incremental union edits

* refactor(control-ui): isolate scalar edit session state

* fix(control-ui): keep scalar edit branch type internal

* fix(control-ui): avoid detached focus selector

* fix(control-ui): round-trip exact numeric branches

* refactor(control-ui): share exact scalar formatting

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hiroshi Tanaka
2026-08-21 00:50:24 +09:00
committed by GitHub
parent 4768ac53c5
commit 55f1738d50
14 changed files with 1193 additions and 69 deletions
+20
View File
@@ -8,6 +8,11 @@ on:
required: false
default: ""
type: string
capture_ui_proof:
description: Capture and upload sanitized Control UI screenshots from real-Gateway tests.
required: false
default: false
type: boolean
include_android:
description: Run Android lanes for this manual CI dispatch.
required: false
@@ -1746,6 +1751,9 @@ jobs:
- *cache_playwright_chromium
- *install_playwright_chromium
- name: Build Control UI bundle for real-Gateway tests
run: pnpm ui:build
- name: Test MCP app conformance with a real Gateway
run: >-
node scripts/run-vitest.mjs run
@@ -1754,12 +1762,24 @@ jobs:
ui/src/e2e/mcp-app-conformance.e2e.test.ts
- name: Test Control UI auth transports with a real Gateway
env:
OPENCLAW_CAPTURE_UI_PROOF: ${{ github.event_name == 'workflow_dispatch' && inputs.capture_ui_proof && '1' || '0' }}
OPENCLAW_UI_E2E_ARTIFACT_DIR: .artifacts/control-ui-e2e/real-gateway
run: >-
node scripts/run-vitest.mjs run
--config test/vitest/vitest.ui-e2e.config.ts
--configLoader runner
ui/src/e2e/control-ui-auth-transports.e2e.test.ts
- name: Upload sanitized Control UI real-Gateway proof
if: always() && github.event_name == 'workflow_dispatch' && inputs.capture_ui_proof
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: control-ui-real-gateway-proof-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/control-ui-e2e/real-gateway
if-no-files-found: error
retention-days: 14
- name: Test Control UI Logs lifecycle with a real Gateway
run: >-
node scripts/run-vitest.mjs run