* fix(plugin-sdk): guard provider catalog live URL parsing against malformed responses
Provider catalog live discovery now wraps \
ew URL()\ calls in a
\ ryParseUrl\ helper that returns undefined instead of throwing
TypeError on malformed absolute URLs from API responses.
Before: a malformed \
ext\ / \links.next\ URL or an unparseable
fetch redirect target in the catalog response body crashes model
discovery with an uncaught TypeError.
After: malformed URLs are treated as absent (no next page), falling
through to cursor-based pagination or completing the catalog.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(plugin-sdk): return incomplete pagination when malformed next URL has no cursor fallback
When a provider catalog response advertises a next page via the `next`
or `links.next` field but the URL is malformed or cross-origin, the
previous fix fell through to bodyAdvertisesMoreLiveModelCatalogPages
which could silently return `complete` when has_more was absent or
false. This truncated catalogs that use only link-based pagination.
The fix returns `incomplete` when a provider-advertised next URL is
unusable and no cursor-based fallback exists, surfacing a controlled
error instead of silently dropping pages.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(ai): invert plugin coupling behind the transport host port
* fix(ai): queue custom transport registrations until the host is configured
* refactor(ai): remove relocated transport sources from src/agents
* fix(ai): source core stream types from canonical packages and fix tarball fixtures
* fix(ai): invert plugin transport host wiring
* fix(ai): harden managed transport projection
* test(ai): register synchronous stream in transport mock
* fix(ai): lazily install transport runtime host
* fix(ai): preserve completion compat detection
* fix(plugin-sdk): guard assertHttpUrlTargetsPrivateNetwork against malformed URLs
Wrap new URL() in try/catch so public SSRF helper callers get a typed
Invalid URL error instead of an uncaught TypeError from the URL parser.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugin-sdk): use stable Invalid URL error for malformed SSRF input
Stop echoing caller endpoint strings in parse failures and add coverage
that credential-bearing malformed URLs are not reflected in errors.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(plugin-sdk): drop native URL error cause from SSRF guard
Do not attach the native ERR_INVALID_URL as cause when wrapping
malformed URL parser failures. Node's URL parser retains the
rejected URL in its structured input property, so preserving
it as cause leaves credential-bearing endpoint strings reachable
to callers that inspect or serialize the complete error graph.
Extend the credential-reflection regression test to assert the
cause is absent and the full error serialization is clean.
* fix(plugin-sdk): preserve TypeError contract for malformed URL failures
Throw sanitized TypeError("Invalid URL") instead of plain Error to
preserve the public plugin SDK error contract. Callers may rely on
instanceof TypeError to distinguish malformed endpoint input from
other error classes.
* chore: retry CI (pre-existing child-process flake unrelated to ssrf-policy change)
* fix(plugin-sdk): preserve ERR_INVALID_URL code in sanitized TypeError
Add code: "ERR_INVALID_URL" to the sanitized TypeError so callers
that classify malformed URLs through err.code continue to work.
Still strips the native input and cause to prevent credential reflection.
* test(plugin-sdk): trim malformed URL guard coverage
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugin-sdk): bound Anthropic Vertex ADC credential read
canReadAnthropicVertexAdc read the entire GOOGLE_APPLICATION_CREDENTIALS
file via unbounded fs.readFileSync solely to check readability (the
contents are discarded). The provider-local copy of this logic in
extensions/anthropic-vertex/region.ts was already bounded with
tryReadSecretFileSync + a 1 MiB limit in #109260 ("reject oversized
credential files in remaining readers"); this standalone plugin-sdk
preflight helper is a duplicate that sweep missed.
Mirror the region.ts bound: replace the unbounded readFileSync with
tryReadSecretFileSync(..., { maxBytes: 1 MiB, rejectHardlinks: false }),
wrapped in try/catch (the helper throws FsSafeError on oversize) so an
oversized credential file is rejected instead of slurped into memory.
Presence-check semantics are unchanged for normal-sized files.
* refactor(plugin-sdk): remove orphaned vertex auth helper
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* perf(state): cap the per-agent SQLite handle cache with LRU eviction
Multi-tenant hosts open one WAL database per agent (~3 file descriptors
each); the process-local handle cache was unbounded, so large fleets
exhausted descriptors. Cache hits now refresh LRU recency, and cache-miss
opens evict the oldest non-transactional handle before constructing the
new one, capped at 64 open handles. Eviction closes the process-local
handle only; registry rows and durable data are untouched and evicted
databases reopen transparently on next access.
* fix(state): fleet-wide introspection reads stop opening writable agent databases
Live testing an 81-agent gateway surfaced three full-fleet sweeps that
opened every agent's SQLite database writable (schema ensure + registry
write transaction per open): the 60s health snapshot's per-agent session
summaries, usage-cost cache reads behind the gateway usage endpoints, and
the zalouser doctor detector that ran from every CLI startup. Each sweep
churned the new bounded handle cache and saturated the event loop.
Session listing gains a readonly non-registering variant (SDK exposes it
as an additive readOnly flag on listSessionEntries); health and usage
reads use it, with transient-lock tolerance owned by the health caller.
The zalouser detector gates on the channel/credentials actually existing,
collects once per pass, and detection reads go readonly while migration
apply stays writable. The usage refresh queue's flat 50ms busy retry
becomes exponential backoff capped at 5s, and refreshing summaries retain
their cache timestamp so the 30s TTL gates fleet rescans.
* perf(state): make per-agent handle eviction and reopen cheap
Eviction only works if reopen is cheap. Reopens after eviction previously
repeated full first-open work; ensureOpenClawAgentSchema takes BEGIN
IMMEDIATE and the registry upsert writes the shared state DB, so reopens
blocked in synchronous busy waits (observed 14-22s event-loop stalls with
reconcile workers holding write transactions). Owner/schema validation and
registration now run once per path per process with invalidation on
quarantine and disposal; the read-only integrity guard still runs on every
physical open. Eviction closes with a PASSIVE WAL checkpoint instead of
TRUNCATE, which waits on readers; orderly dispose keeps TRUNCATE so
sidecars are flushed before unlink.
* ci: regenerate plugin-SDK API baseline, drop prod test-seam exports, mock readonly accessor
The additive readOnly flag on the SDK listSessionEntries changed the
declaration surface, so the generated API baseline is regenerated via the
sanctioned script. Knip forbids production exports consumed only by tests:
the usage-cache testApi export is removed outright and the refresh-queue
seam moves behind an env-gated global exposed by a test-support module.
health.plugins.test's full session-accessor mock gains the newly imported
readonly listing.
* feat(channels): batch 1 producers drop media placeholder bodies
Media-placeholder program batch 1: Google Chat, Zalo, LINE, and
Mattermost stop minting <media:kind> placeholder bodies. Media-only
messages carry an empty caption plus one structured fact per native
attachment (type-only when a download fails or is rejected, so payload
positions and kind signals stay aligned). The shared
formatMediaPlaceholderText SDK formatter renders text-only carriers
(Mattermost pending-room lines) from structured facts; per-channel
placeholder builders and the expected-count side channel are deleted.
* fix(mattermost): satisfy type, deadcode, and SDK manifest gates
* feat: add session discussion panel seam
* fix: keep discussion iframe cookie-capable and changelog release-owned
* test: cover cookie-capable discussion iframe sandbox
* fix: stretch discussion panel host so the embed fills the rail
* fix: keep provider failures retryable and probe discussion availability before showing the action
* fix: block same-origin discussion embeds, show action on catalog sessions, close stale panel on reconnect
* fix: scope discussion probes and panel callbacks to the issuing connection
* fix: dedupe in-flight discussion probes per session
* fix: retry superseded discussion probes and key-scope panel results
* fix: regenerate Swift protocol models and extend advertised-method expectations
* style: format chat-pane-header
* fix: regenerate Kotlin protocol models and date discussion methods in the 2026.7 train
* chore: restore release-owned changelog to main state
* chore: keep changelog untouched relative to merge-base
* feat(browser): send pages to the main session from the Chrome extension
One-click page share in the OpenClaw Chrome extension: toolbar popup with an
optional note, page/selection context menu, and Alt+Shift+S. Capture is
selection-first with a readability heuristic, X/Twitter thread extraction, and
Google Docs plain-text export via the user's session cookies. Payloads ride the
existing paired relay WebSocket as a new pageShare message; the gateway-only
page-share sink wraps page text in the external-content safety boundary, then
enqueues a main-session system event and requests an immediate heartbeat
(hooks/wake semantics). Node-hosted relays report a clear unsupported error.
Capture heuristics adapted from Nat Eliason's MIT-licensed send-to-openclaw.
Co-authored-by: Codex <codex@openai.com>
* fix(browser): keep page-controlled metadata inside the share safety boundary
Review findings: move title/URL inside wrapExternalContent (a hostile <title>
must not become trusted header text), prefer the user's selection over the
full Google Docs export, and pass the context-menu selectionText through so
iframe selections and selections cleared during relay reconnect still win.
* fix(browser): bind context-menu shares to the click-time document
Selection shares from the context menu now send the click snapshot directly
(no recapture), so navigations during relay reconnect cannot mislabel the
source and iframe selections are preserved. The Google Docs selection probe
scans all accessible frames before falling back to the full-document export.
* test(browser): expect the page-share handler in relay server args
* fix(browser): probe only the main frame for Google Docs selections
All-frame injection rejects wholesale when one frame is inaccessible and
returns child frames in nondeterministic order, so the probe now reads the
main frame only. Child-frame selections still share correctly through the
context menu's click-time selectionText; toolbar/shortcut entry sends the
full page for that case (named tradeoff in the code comment).
* fix(browser): satisfy page-share CI gates
---------
Co-authored-by: Codex <codex@openai.com>
* feat(codex): fork upstream-linked sessions at a message via thread/fork
* fix(gateway): fail closed for rewind and branch switch on upstream-linked sessions
* fix(codex): fail closed on first-message forks, image-only prompts, and orphan archival
* fix(codex): baseline retained history and reject paginated threads on upstream fork
* fix(codex): validate the full fork prefix and fail closed across crash windows
* fix(codex): treat all non-text inputs as unverifiable in fork drift checks
* fix(codex): support first-message forks as empty-history upstream cuts
* fix(codex): reject source-id reuse and unverifiable hidden inputs in fork boundaries
* refactor(codex): materialize upstream forks from verified thread read-back
* fix(codex): satisfy strict type lanes and knip for upstream fork
* fix(status): carry prepared thinking catalog
Resolve status thinking levels from model metadata prepared by each caller.
Model-state paths carry their resolved catalog forward, while standalone status
surfaces make one side-effect-free read without provider discovery.
Co-authored-by: Bartok9 <danielrpike9@gmail.com>
* test(status): assert read-only native catalog load
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): bound busy channel health by real run age
The channel health policy treats a channel as healthy-busy even while
disconnected, bounded only by a 25 minute stale ceiling measured from
lastRunActivityAt. The run-state heartbeat refreshes lastRunActivityAt
every 60 seconds for as long as any run is active, so a run that hangs
forever (for example a send blocking on a dead socket after the
transport already reported connected:false) keeps that timestamp fresh
and the stuck ceiling is never reached. The account is then reported
healthy forever by the health monitor, readiness probe, and health CLI,
and no restart ever fires.
createRunStateMachine now tracks each in-flight run's start time keyed by
an opaque run handle and publishes the oldest still-active run's start as
activeRunStartedAt. The health policy busy override keys its ceiling off
the real run age, so a run stuck longer than the threshold reports stuck
and the monitor can restart it. Because the reported start is the oldest
active run and advances to the next-oldest as runs complete, a channel
churning through many short overlapping runs (activeRuns above 1 across
concurrent queue keys) stays healthy; only a genuinely hung run breaches
the ceiling. Short and active runs stay healthy and the existing
lastRunActivityAt fallback is preserved for snapshots without a start
time.
* fix(channels): retain run-state callback compatibility
Keep the released zero-argument onRunEnd callback source-compatible while allowing internal queue callers to pass a run handle for exact concurrent-run accounting. The compatibility path closes the oldest active run, preserving existing lifecycle behavior for consumers that do not use handles.
* fix(channels): keep anonymous runs out of age tracking
The zero-argument lifecycle callbacks cannot identify which concurrent run completed, so they must not update the identity-sensitive run start used by channel health. Keep their busy count separately and reserve exact start tracking for the shared queue's handle-aware lifecycle path.
* fix(channels): keep tracked runs internal
Keep the public run-state lifecycle callbacks unchanged. The channel queue now owns opaque run identity and augments its status updates with the oldest active queue run, so implementation details do not expand the SDK surface.
* fix(channels): type queue run start status
Keep activeRunStartedAt in the internal status patch type so the queue can publish its private tracked-run age through the existing status sink.
* fix(channels): wrap isActive to satisfy unbound-method lint
* fix(gateway): gate busy run-age ceiling on disconnected transport
Avoid buffering unbounded provider error streams and cover the resource lifecycle with a deterministic cancellation contract.
Co-authored-by: Peter Steinberger <steipete@gmail.com>