* feat: add PDF analysis tool with native provider support
New `pdf` tool for analyzing PDF documents with model-powered analysis.
Architecture:
- Native PDF path: sends raw PDF bytes directly to providers that support
inline document input (Anthropic via DocumentBlockParam, Google Gemini
via inlineData with application/pdf MIME type)
- Extraction fallback: for providers without native PDF support, extracts
text via pdfjs-dist and rasterizes pages to images via @napi-rs/canvas,
then sends through the standard vision/text completion path
Key features:
- Single PDF (`pdf` param) or multiple PDFs (`pdfs` array, up to 10)
- Page range selection (`pages` param, e.g. "1-5", "1,3,7-9")
- Model override (`model` param) and file size limits (`maxBytesMb`)
- Auto-detects provider capability and falls back gracefully
- Same security patterns as image tool (SSRF guards, sandbox support,
local path roots, workspace-only policy)
Config (agents.defaults):
- pdfModel: primary/fallbacks (defaults to imageModel, then session model)
- pdfMaxBytesMb: max PDF file size (default: 10)
- pdfMaxPages: max pages to process (default: 20)
Model catalog:
- Extended ModelInputType to include "document" alongside "text"/"image"
- Added modelSupportsDocument() capability check
Files:
- src/agents/tools/pdf-tool.ts - main tool factory
- src/agents/tools/pdf-tool.helpers.ts - helpers (page range, config, etc.)
- src/agents/tools/pdf-native-providers.ts - direct API calls for Anthropic/Google
- src/agents/tools/pdf-tool.test.ts - 43 tests covering all paths
- Modified: model-catalog.ts, openclaw-tools.ts, config schema/types/labels/help
* fix: prepare pdf tool for merge (#31319) (thanks @tyler6204)
* fix(browser): skip port ownership check for remote CDP profiles
When a browser profile has a non-loopback cdpUrl (e.g. Browserless,
Kubernetes sidecar, or any external CDP service), the port-ownership
check incorrectly fires because we don't "own" the remote process.
This causes "Port is in use but not by openclaw" even though the
remote CDP service is working and reachable.
Guard the ownership error with !remoteCdp so remote profiles fall
through to the WebSocket retry/attach logic instead.
Fixes#15582
* fix: add TypeScript null guard for profileState.running
* chore(changelog): note remote CDP ownership fix credits
Refs #15582
* Update CHANGELOG.md
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(gateway): skip device pairing for local backend self-connections
When gateway.tls is enabled, sessions_spawn (and other internal
callGateway operations) creates a new WebSocket to the gateway.
The gateway treated this self-connection like any external client
and enforced device pairing, rejecting it with "pairing required"
(close code 1008). This made sub-agent spawning impossible when
TLS was enabled in Docker with bind: "lan".
Skip pairing for connections that are gateway-client self-connections
from localhost with valid shared auth (token/password). These are
internal backend calls (e.g. sessions_spawn, subagent-announce) that
already have valid credentials and connect from the same host.
Closes#30740
* gateway: tighten backend self-pair bypass guard
* tests: cover backend self-pairing local-vs-remote auth path
* changelog: add gateway tls pairing fix credit
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: detect PID recycling in session write lock staleness check
The session lock uses isPidAlive() to determine if a lock holder is
still running. In containers, PID recycling can cause a different
process to inherit the same PID, making the lock appear valid when
the original holder is dead.
Record the process start time (field 22 of /proc/pid/stat) in the
lock file and compare it during staleness checks. If the PID is alive
but its start time differs from the recorded value, the lock is
treated as stale and reclaimed immediately.
Backward compatible: lock files without starttime are handled with
the existing PID-alive + age-based logic. Non-Linux platforms skip
the starttime check entirely (getProcessStartTime returns null).
* shared: harden pid starttime parsing
* sessions: validate lock pid/starttime payloads
* changelog: note recycled PID lock recovery fix
* changelog: credit hiroki and vincent on lock recovery fix
---------
Co-authored-by: HirokiKobayashi-R <hiroki@rhems-japan.co.jp>
* docs(gateway): document Docker bridge networking and loopback bind caveat
The default loopback bind makes the gateway unreachable with Docker
bridge networking because port-forwarded traffic arrives on eth0, not
lo. Add a note in both the Dockerfile and the configuration reference
explaining the workarounds (--network host or bind: lan).
Fixes#27950
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(docker): note legacy gateway.bind alias migration
* docs(gateway): clarify legacy bind alias auto-migration
* docs(docker): require bind mode values in gateway.bind
* docs(gateway): avoid bind alias auto-migration claim
* changelog: add #28001 docker bind docs credit
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* Fix onboard ignoring OPENCLAW_GATEWAY_TOKEN env var
When running onboard via docker-setup.sh, the QuickStart wizard
generates its own 48-char token instead of using the 64-char token
already set in OPENCLAW_GATEWAY_TOKEN. This causes a token mismatch
that breaks all CLI commands after setup.
Check process.env.OPENCLAW_GATEWAY_TOKEN before falling back to
randomToken() in both the interactive QuickStart path and the
non-interactive path.
Closes#22638
Co-authored-by: Clawborn <tianrun.yang103@gmail.com>
* Tests: cover quickstart env token fallback
* Changelog: note docker onboarding token parity fix
* Tests: restore env var after non-interactive token fallback test
* Update CHANGELOG.md
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>