* fix(cli): announce when nodes list degrades to paired-only data
tryReadNodeList swallowed every enrichment failure, so the unfiltered nodes list silently rendered a table without connected/commands state. The fallback is now announced on stderr, keeping --json stdout parseable.
* fix(gateway): silently widen local pairing scopes as documented
`autoApproveLocal` has documented "silently approves pairing, role upgrades,
and scope upgrades from trusted local connections" since the loopback
auto-approval landed, but a later hardening pass forced every scope-upgrade
pairing request non-silent. That block protected nothing locally — silent
initial pairing grants a fresh identity arbitrary requested scopes, so any
local process could mint a new keypair instead of upgrading — while it
stranded every row-authorized client (CLI, native apps, node hosts) on a
manual approval no local surface could perform.
Scope upgrades now ride the same silent-local rule as initial pairing, with
one new restriction that encodes the real boundary: the connect must itself
prove local-grade credentials (auth mode none, or the shared token/password).
Identity-proxy connects (tailscale, trusted-proxy) and bearer device tokens
never did, so their pairing rows remain a durable scope cap, and
`autoApproveLocal: false` still forces manual approval for everything.
The silent self-grant also approves the union of requested plus already-held
scopes: approval merges the existing row back in, so a client requesting only
its missing scope no longer fails the caller-authority check.
The decision surface shrinks with the behavior change: the scope-upgrade veto
contradicted shouldAllowSilentLocalPairing's answer, the CLI-container
locality was a duplicate of the shared-secret-loopback predicate, and three
classifiers re-derived the same shared-secret auth check.
Live-verified on an isolated auth-none loopback gateway: a CLI identity
paired at operator.pairing silently widens to operator.read on the next wider
command, with the scope-upgrade security audit line still emitted.
* test(gateway): rewrite veto-era pairing locks for silent local widening
Five control-ui pairing suite cases and the silent-scope-upgrade poc locked
the removed non-silent veto. The suite cases now assert the new invariant
(local shared-auth upgrades widen silently, malformed and legacy-shaped rows
are repaired by the fresh approval, node-then-operator grants complete
without a stranded prompt), and the poc case now exercises the surviving
manual-approval gate by disabling autoApproveLocal after its watcher
connects, keeping the pairing-request broadcast and remediation-hint
assertions alive on a real remaining path. The voice-node bootstrap failure
was leakage from the aborted sibling tests, and passes again once they
complete their flows.
* fix(computer-use): unblock the macOS live-rig proof flow
The rig ran its operator CLI and its proof runner from one state dir, so both
shared one device identity. A paired operator device is pinned to the scopes of
its first connect, and `nodes list` connects first for `node.pair.list`
(operator.pairing); the proof runner then needs operator.write, which is a scope
upgrade the gateway never approves silently and which no rig client can approve
for itself. The proof runner is a GATEWAY_CLIENT/BACKEND client, so on a
loopback auth-none gateway it is admitted unpaired with the scopes it asks for:
giving the CLI its own `cli-state` identity is enough, and `agent-state` now
never accumulates a pairing row.
`nodes list` also read `node.list` through the plain CLI client while
`nodes status`/`describe` used the diagnostics ladder. On any gateway where the
CLI must pair, the unfiltered list silently dropped connected/commands/
computerUse and `--connected` failed outright, so the documented rig gate could
not confirm the node. Both call sites now use `callNodeDiagnosticsGatewayCli`.
Docs drop the `devices approve <requestId>` instruction, which was circular:
that invocation is its own new device identity.
* test(cli): share the runtime-log formatter across nodes CLI e2e files
The extracted diagnostics-auth file stringified captured log arguments directly, which the type-aware core lint stripe rejects (no-base-to-string). Move the existing formatter into the shared node test helpers instead of duplicating it.
Four nodes-CLI repairs at their owners:
- nodes status --last-connected joined a second pairing-scoped RPC
(node.pair.list) to reconstruct last-connection times the gateway already
records on every node.list row (lastConnectedAtMs, max of stored pairing
history and live connection, since 2dcd47d4f4). The client-side join also
preferred stored history over a live connection instead of taking the max,
and made the command fail for callers whose auth grants operator.read but
not operator.pairing. The filter now reads the recorded fact;
connectedAtMs covers gateways predating it.
- nodes list --connected blanked pending rows and then printed "Pending: 0"
while requests waited — asserting a fact it never checked. Pending rows
carry no connection state to filter on; they now always show.
- nodes remove/rename carried unreachable !nodeId guards (resolveCliNodeId
throws "node required" on blank input and every parse layer drops id-less
rows) whose dead text pointed at the wrong command (nodes pending targets
pairing requests, not paired nodes). Guards deleted; the reachable empty
--name branch now hints at nodes list.
- status.node-mode derived node-service liveness from the formatted
runtimeShort display string — parsing a fact back out of its own
projection, dead in all first-party call paths (both callers pass the
runtime object it is derived from). Field and branch deleted.
e2e test updated to pin the single-RPC shape (asserts node.pair.list is NOT
called); node-mode fixtures now use the production shape.
* fix(cli): make commands, completion, and JSON output reliable
* fix(cli): reconcile completion coverage with current main
* test(cli): keep test routing stable across isolation lanes
* feat(linux): add node device capabilities
* fix(linux-node): actionable pending-approval error + node-host advertise integration test
* fix(linux-node): map geoclue access-denied to LOCATION_DISABLED; floor camera maxWidth to avoid zero-height scale
* fix(linux-node): clamp small camera maxWidth to 2 instead of default
* docs(linux-node): clarify where-am-i -t is a process timeout, not update throttle
* refactor(gateway): extract legacy-node filter + rejection hint to fit LOC ratchet; docs-map + deadcode baseline
* fix(gateway): drop now-unused DEFAULT_DANGEROUS_NODE_COMMANDS import after hint extraction
* test(node-host): drop imports orphaned by removed error-code test
* feat(nodes): route alerts by active computer
* fix(ci): allowlist node presence mobile coverage
* test(prompts): refresh node presence snapshots
* fix(macos): await presence reporter actor hop
* fix(macos): type presence test delivery state
* docs(nodes): add active presence guide
* docs(nodes): format presence troubleshooting
registerNodesCli unconditionally registered plugin CLI commands, so
lightweight built-in commands like `nodes status`/`nodes list` paid the
full plugin CLI/runtime load cost. Only resolve plugin-provided node
subcommands (e.g. `nodes canvas`) when the invoked subcommand is not
already a built-in, keeping the built-in nodes path fast.
Fixes#96697
Extract shared normalization/coercion helpers into private @openclaw/normalization-core workspace package while preserving existing plugin SDK helper subpaths.\n\nAlso keeps direct normalization-core imports internal, wires UI/build/loader resolution, and replaces the slow PR network CodeQL lane with a fast added-line boundary scan while retaining full CodeQL for scheduled/manual runs.\n\nVerification: local moved tests, plugin SDK boundary tests, extension loader tests, agents-support shard, UI build/test, build artifacts, lint, workflow guards, autoreview, and GitHub CI passed on PR head 963d893715.
* feat(browser): add optional vision understanding to screenshot tool
* fix(browser): wrap vision output as external content, enforce maxBytes, forward auth profiles
* fix(browser): remove no-op scope/attachments config, drop profile pass-through lacking runtime support
* feat(media-understanding): add profile/preferredProfile to DescribeImageFileWithModelParams and forward to describeImage
* style(browser): add curly braces to satisfy eslint curly rule
* fix(browser): correct tools.browser.enabled help text to match actual behavior
* fix(browser): thread agentDir/workspaceDir from plugin tool context into browser vision
* refactor(browser): move vision config from tools.browser to browser.models
The browser plugin's vision configuration now lives on the top-level
`browser` config namespace (browser.models, browser.visionEnabled,
browser.visionPrompt, etc.) instead of `tools.browser`. This aligns
with the plugin's existing config location and avoids confusion between
tool-level and plugin-level settings.
- Remove tools.browser from ToolsSchema and ToolsConfig
- Add models/vision* fields to BrowserConfig and its zod schema
- Update getBrowserVisionConfig to read from cfg.browser
- Update schema help, labels, and quality test
- Update vision.test.ts to use new config shape
* docs(browser): add screenshot vision configuration section
Document the new browser.models config for automatic screenshot
description via vision models, enabling text-only main models to
reason about web page content.
* fix(browser): remove deliverable media markers from vision result, drop unused import
P1: Vision-success path no longer exposes the raw screenshot as
deliverable media (removes MEDIA: line and details.media.mediaUrl).
This prevents channel delivery from auto-sending sensitive page content
when the intended output is a text description.
P2: Remove unused ToolsMediaUnderstandingSchema import that would fail
noUnusedLocals typecheck.
* fix(browser): add command/args fields to browser models schema
The browser vision model schema uses .strict(), so CLI-type entries
with command/args were rejected by TypeScript. Add these fields to
align with MediaUnderstandingModelSchema.
* chore(browser): remove debug console.log statements
* fix(browser): harden screenshot vision result against MEDIA: directive injection and restore image sanitization on failure fallback
ClawSweeper #84247 review round 2:
P1 (security, high): neutralize line-start MEDIA: directives in vision descriptions
before wrapping with wrapExternalContent. The agent media extractor scans every
browser tool-result text block via splitMediaFromOutput which treats line-start
MEDIA: as a trusted local-media delivery directive, and browser is on the
trusted-media allowlist. Without neutralization, page or vision-provider output
containing 'MEDIA:/tmp/secret.png' could synthesize a channel-deliverable media
artifact from untrusted content. wrapExternalContent itself does not strip
line-start directives. Introduce neutralizeMediaDirectives in vision.ts that
prepends '[neutralized] ' to any line whose trimStart() begins with MEDIA:
(case-insensitive), defanging the parser anchor while keeping the original
text human-readable.
P2 (compatibility): pass resolveRuntimeImageSanitization() to imageResultFromFile
in the vision-failure catch fallback. The non-vision screenshot path already
forwards this option (d5cc0d53b7) so configured agents.defaults.imageMaxDimensionPx
takes effect. Without this fix, any provider timeout/error silently bypasses the
sanitization guard and returns a raw full-resolution screenshot.
Regression coverage:
- vision.test.ts: 6 unit cases for neutralizeMediaDirectives (no-op fast path,
mid-line MEDIA: untouched, line-start defanged, leading-whitespace defanged,
case-insensitive, multiple directives per blob).
- browser-tool.test.ts: 2 integration cases that drive the full screenshot
tool execute path:
- 'neutralizes MEDIA: directives in vision text and does not attach media'
asserts no line matches /^\s*MEDIA:/i in returned text, secret path text
is preserved verbatim, details.media is absent, and imageResultFromFile
is not called on the success path.
- 'preserves screenshot image sanitization on vision failure fallback'
mocks describeImageFileWithModel to reject and asserts the fallback
imageResultFromFile call receives imageSanitization: {maxDimensionPx:1600}
plus the 'browser screenshot vision failed' extraText.
* fix(browser): apply clawsweeper fallback media fix from PR #84247
* refactor: reuse media image understanding for browser screenshots
* refactor: use structured media delivery
* test: update music completion media instruction expectation
* fix: trim buffered reply directive padding
* test: refresh codex prompt snapshots for message media aliases
---------
Co-authored-by: scotthuang <scotthuang@tencent.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Summary:
- The PR extracts JSON-mode console-to-stderr routing into a shared CLI helper, wraps root and `nodes` lazy plugin registration, adds nodes registration coverage, and adds a changelog entry.
- Reproducibility: yes. for source-level reproduction: the linked report shows `openclaw nodes list --json 2> ... ssing the existing JSON stderr guard. I did not run the live Helm/container repro in this read-only review.
Automerge notes:
- PR branch already contained follow-up commit before automerge: Route JSON-mode plugin registration logs to stderr
Validation:
- ClawSweeper review passed for head c9d0867db0.
- Required merge gates passed before the squash merge.
Prepared head SHA: c9d0867db0
Review: https://github.com/openclaw/openclaw/pull/84741#issuecomment-4503741078
Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>