resolveSshConfig and startSshPortForward spawned a hard-coded /usr/bin/ssh,
so SSH config discovery and gateway tunneling failed wherever the system ssh
client lives elsewhere: Windows (built-in OpenSSH under System32\OpenSSH) and
NixOS (/run/current-system/sw/bin).
Route both helpers through the existing resolveSystemBin("ssh", { trust:
"strict" }) resolver, and add the Windows built-in OpenSSH directory to the
resolver's trusted Windows locations (it had System32 but not the
System32\OpenSSH subdirectory where ssh.exe actually ships). Fail closed with
a clear diagnostic when no system ssh client is present.
Closes#83289.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(talk): emit realtime models and voices in talk.catalog and mirror create-time readiness
* feat(ui): add curated Talk settings page with catalog-driven pickers
* docs(talk): correct stale claims and add one-page GPT-Live setup path
* fix(ui): refresh Talk catalog on config-hash advance and neutralize GPT-Live badge
* fix(ui): provider-aware Talk selection, atomic provider switch, focus refresh
* fix(ui): resolve Talk provider fallbacks via catalog and make Default a true reset
* fix(ui): provider-compatible transports and race-free Talk catalog loads
* fix(ui): never resolve an unknown explicit Talk provider to the active one
* docs(talk): note Android relay readiness caveat for browser-only models
* fix(ui): keep the relay transport when switching Talk provider to Auto
* fix(ui): align section-ownership test and drop unused export after rebase
* feat(gateway): share the canonical browser-origin policy with plugins
Export resolveAcceptedBrowserOrigin through openclaw/plugin-sdk/webhook-request-guards
so browser-facing plugin routes reuse the Gateway's real origin contract instead of a
narrow allowedOrigins array check. Private LAN/Tailnet Control UI loads and the
Host-header fallback were previously rejected with 403 by plugin offer routes while the
Control UI itself worked.
Moves the loopback/forwarded-header helpers to net.ts (re-exported from auth.ts) so the
guard can delegate without importing gateway auth, and migrates the Codex realtime
broker onto the shared seam.
* feat(talk): let providers own agent delegation for realtime voice
Adds an optional runAgentConsult callback to the browser-session create request and
injects the existing embedded consult runtime from talk-client, bound to the same agent
and session key the GA tool path uses. Providers whose realtime protocol delegates work
through their own control channel (rather than GA function calls) can now reach the
OpenClaw agent without a client round-trip.
Threads the effective per-session model into browser-session capability resolution so a
request-level model override selects the right capability set, and propagates a caller
abort signal into consultRealtimeVoiceAgent so a superseded delegation stops its run.
* feat(openai): support GPT-Live realtime voice over ChatGPT OAuth
Implements OpenAI's quicksilver/frameless session natively for Talk browser sessions.
The Gateway creates the WebRTC call (multipart sdp+session to https://api.openai.com/v1/live)
and owns the sideband control socket, so the browser never holds upstream credentials;
delegation.created events run through the OpenClaw agent and stream back as speakable
context appends.
Verified end-to-end on 2026-07-28 against a ChatGPT Pro OAuth profile: call create 201
with an rtc_* id and answer SDP, sideband session.started, session.close teardown.
ChatGPT OAuth is preferred over a Platform API key because /v1/live access for platform
keys is waitlist-gated; the legacy chatgpt.com backend route returns 403 for every model
and protocol version and is not used.
Accepted models are gpt-live-1-codex and gpt-live-1-boulder-alpha; the voice allowlist is
the ten values the route actually accepts, since an invalid voice is rejected at call
creation and cannot be repaired afterwards (session.update reports immutable_field_update).
* docs: document GPT-Live Talk support and its route gotchas
Records the working route and auth, the accepted models and voices, the browser-only
scope, and the two traps that cost the most time: the chatgpt.com backend route returns
403 Voice session access denied for every model, and that same 403 is also what an
invalid voice returns, so it must not be read as an account entitlement block.
* fix(openai): resolve GPT-Live CI failures
* refactor(openai): own zod runtime dependency
* fix(openai): satisfy lint and live-shard gates for GPT-Live
Types the retry-delay finish callback as Error so the rejection reason is provably an
Error at the call site; the abort path already normalized a non-Error AbortSignal reason,
but the unknown parameter type hid that from static analysis.
Registers the new GPT-Live live test in the native-live-extensions-openai shard
expectation. The shard selector already picked the file up from the real tree; only the
hardcoded list in the tooling test lagged.
* fix(openai): clean up post-rebase capability resolver
* fix(openai): preserve GPT-Live delegation fragments
* fix(openai): close GPT-Live sideband handoff race
* fix(openai): accept UUID GPT-Live call ids
* style: apply oxfmt to GPT-Live sources and Talk docs
* style: format Talk docs after rebase
* fix(openai): keep GPT-Live transcript context across ignored delegations
* origin/test/parallels-main-3224005-host:
docs(changelog): cover Windows prerequisite readiness
fix(e2e): preserve Windows installer reboot results
docs(changelog): include Windows stop safety gate
fix(e2e): force Windows smoke gateway stop
docs(changelog): note Parallels and update fixes
fix(update): finalize install switches in fresh process
fix(e2e): harden Parallels guest preparation
* origin/main: (72 commits)
fix: macOS-only Parallels smoke harness regression (#75293)
fix(ios): use configured speech language for system voice (#113372)
refactor: centralize isolated agent test ownership (#115340)
test(ui): stabilize browser route readiness (#115320)
fix(cron): clear automation after failed scheduler shutdown (#115316)
fix: keep isolated gateway workspaces and skills inside state dir (#114487)
docs: document local TTS on macOS, Linux, and Windows (#110230)
fix(tasks): clone only the requested task page (#114277)
fix(agents): prevent Code Mode resume loss and unbounded preparation (#115314)
fix(process): preserve the first process cancellation reason (#115317)
fix(nostr): report unavailable ingress when queue open fails (#115313)
fix: prevent Slack and Feishu ingress startup leaks (#115295)
fix(slack): avoid unsafe emphasis boundaries (#111575)
fix: prevent changed checks from expanding into full lint (#115322)
fix(codex): stabilize prompts and expose real cache regressions (#115238)
refactor(ui): derive page route specs from the route table (#115214)
fix(compaction): plan cut points in trigger units (#111886) (#115309)
fix: prevent cron and gateway stress-run timeouts (#115241)
fix(ci): validate frozen Matrix QA targets (#115298)
test(macos): block real process fallthrough (#113337)
...
# Conflicts:
# CHANGELOG.md
* fix(tooling): check changed exports for dead code
* fix(tooling): name mismatched PR wrapper components
* feat(tooling): repin PR review artifacts
* fix(tooling): scope dead-export detection to source files
Matching only the top-level tree let a docs file under src/ carrying `export`
in a code sample set the flag. That mattered because the flag also short-
circuited changedCheckRequiresRemote above its docsOnly guard, so a docs-only
change could be dragged onto the heavy remote route and made to run knip.
Scope detection to the source extensions knip actually reads, and drop the
routing branch entirely: any file that can now trigger detection already
enables a non-docs lane, which the existing final clause routes remotely. The
branch was dead for every real source change and wrong for the docs case.
* refactor(tooling): round-trip the repinned review JSON
Preserving byte-exact formatting required a hand-rolled JSON scanner: string
escape handling, container nesting, and span splicing, for a file that lives in
gitignored .local/ and is only ever read back through JSON.parse.
repin already parses the artifact to validate it, so assign the two identity
fields and re-serialize. JSON.parse/stringify keeps insertion order, which is
the only formatting property worth holding. Drops four helpers.
* fix(tooling): select the dead-export scan by path, not changed lines
Inspecting changed lines for an `export` token has two false negatives that
defeat the purpose. Barrels export through multiline `export { ... }` lists, so
removing a symbol changes a line carrying no `export` token. Worse, the failure
that motivated this scan was an import-only edit: it orphaned a re-export in a
barrel the diff never touched, which no changed-line rule can see.
Select by path instead: any changed production source file under src/,
extensions/, ui/, or packages/. This also removes the merge-base dependency, so
a shallow checkout or unrelated history can no longer silently skip the scan.
Those paths already enable a non-docs lane, so the run already routes remotely
and knip's cost lands on the box already doing the heavy work.
OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE=1 still opts out.
* Revert "feat(tooling): repin PR review artifacts"
The repin subcommand defeats the property the identity pin exists to enforce.
It rewrites the pinned PR number and head SHA while keeping every
recommendation, finding, and Markdown conclusion, so a completed
"READY FOR /prepare-pr" verdict authored against one revision can be relabelled
as covering another and then pass validation. The test added alongside it
restamped PR #7 as PR #42 and asserted validation succeeded, which is exactly
the substitution the fail-closed gate was built to stop.
Re-authoring a review after the head moves is friction on purpose: the code the
verdict describes may no longer be the code being landed. A safe version has to
prove the reviewed content is equivalent across the move, which is a design
decision about what equivalence means, not a convenience wrapper.
Reverts 5d89a704d9 and its follow-up c6304eb539.
* docs(tooling): note why the wrapper diagnostic reads HEAD blobs
Two reviewers independently read the new component comparison as inspecting
committed state while the refusal was triggered by working-tree files. The
uncommitted-wrapper guard above already exits for any staged or unstaged edit to
these three paths, so HEAD matches the working tree by the time this runs. State
the invariant at the site instead of letting a third reader re-derive it.
* fix(tooling): cover jsx in the dead-export source selector
The hand-written extension alternation matched .tsx but not .jsx, so a JSX
source change would skip a scan that knip does apply to it. Use the
`[cm]?[jt]sx?` selector the lint lanes in check-changed.mjs already use, which
covers both and is shorter, and pin the extension coverage in tests.
* perf(sqlite): cache synchronous prepared statements
* fix(sqlite): refresh cached statement metadata
* fix(sqlite): bound cached statement retention
* fix(sqlite): close statement cache lifecycle gaps
* test: suppress SQLite warning in retention child
* test(sqlite): make transcript-load counter statement-cache-aware and allowlist raw test SQL
* test(sessions): type the wrapped iterate against its overloaded signature
* test: share a statement-cache-aware SQLite execution counter across prepare-count tests
* fix(pr): validate head SHA with bash regex so fork-CI runners without ripgrep pass the artifacts-init guard
---------
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(config): give shared channel settings help text
* fix(ui): space the settings tiers and stop repeating help on list items
* feat(ui): link each channel detail to its docs page
* test(ui): mirror production channel hints in the mock dev server
* docs: note the shared channel help inheritance for plugin authors
* fix(config): let a channel suppress shared help with an empty string
* chore(config): regenerate bundled channel metadata for the new WhatsApp hints
* fix(ci): check bundled channel metadata locally
* chore(release): keep changelog release-owned
* chore(release): keep changelog release-owned
* fix(ui): keep shared channel help contract-neutral
scripts/pr review artifacts carried no PR identity, so a review authored for
another PR validated clean and could gate someone else's landing. Stamp both
review.json and review.md with the reviewed PR number and head, anchor
pr-meta.json to the guarded pr-meta.env, and re-stamp foreign artifacts on init
instead of silently adopting them. review-claim now works in the PR worktree so
no subcommand writes review state to the shared canonical checkout.