* fix(codex): reclaim binding ownership when sessions are deleted
Prepare harness deletion outside the SQLite writer and compare-delete exact companion ownership at the session removal edge, with compensation before commit and native subscription release afterward. Preserve live siblings and upstream native history. Repair proven orphan bindings through the existing Doctor planner under maintenance ownership.
* fix(codex): reuse prepared auth for resumed thread ownership
* docs(plugins): clarify shared migration and auth contracts
* test(codex): use the canonical config type facade
* fix(codex): preserve native threads during manual resume
Record pending attachment intent and validate the immutable native tool catalog
before applying configuration to the selected thread. Require uninterrupted
physical-client ownership through native unsubscribe, resume, and binding commit.
Keep the thread intact on unsupported policy, competing work, or failed proof.
Reuse canonical retained-owner cleanup and preserve unsafe-client retirement.
Cover the real command producer, cold and loaded resumes, competing leases,
lazy-store commit fencing, and the Gateway competing-owner path.
* test: isolate Codex auth and session completion fixtures
* fix(plugins): fail closed when plugin runtime is unavailable
Core fabricated an empty object as `PluginRuntime` for the `cli-metadata`
and `setup-only` registration passes, so any plugin touching `api.runtime`
during `register()` died with an opaque
`TypeError: Cannot read properties of undefined (reading 'openSyncKeyedStore')`
that named neither the contract nor the mode.
Replace those fabrications with one shared fail-closed runtime that throws a
named, actionable error identifying the plugin and registration mode. Symbol
reads stay inert so inspection and reflection cannot trigger the guard.
Also resolve the lightweight `cli-metadata` entry beside the resolved plugin
entry, not only at the package root. Published plugin packages emit it at
`dist/cli-metadata.js`, so the probe always missed and the loader fell back to
executing the full heavy entry for CLI metadata collection.
Documents which registration modes expose a live `api.runtime`; the SDK
reference previously claimed it was injected into every plugin.
* fix(ui): restore sidebar lazy import boundaries
Remove the redundant viewer-facepile dynamic import: sidebar consumers
already register that element through the static graph.
Import pet data and sprite helpers from their owning leaf modules, delete
the registration module's re-export barrel, and make the element itself
the lobster-pet.runtime.ts lazy boundary. Keep shared sprite styles with
the look renderer so standalone pages do not need element registration.
Regenerate the measured boot inventory with the canonical browser probe.
Use Lit's typed lifecycle map to remove the obsolete assertion baseline
entry and keep the now-private facepile variant type local.
The full build has no INEFFECTIVE_DYNAMIC_IMPORT warnings. Relevant UI
coverage passes (503 tests plus 5 Chromium E2E tests); the unchanged plugin
runtime regression suites pass all 58 tests. The broad plugin suite still
reports schema-version, doctor-closure, hook-process, and catalog failures.
Make ask_user use native Telegram controls when one single-select question can be represented directly, while keeping multi-select and batch questions on the text fallback. Add native Other-to-ForceReply input, preserve Gateway-owned option ordering across Telegram, Slack, and Discord, and prevent later progress from overwriting the delivered question.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
The bundled imap plugin landed in #130230 without regenerating the plugin
inventory docs, so pnpm plugins:inventory:check failed on main and the plugin
was absent from the reference index. Regenerate, and register its operator
guide in PLUGIN_DOC_ALIASES so the generated reference page links to
/automation/imap (same mechanism codex and firecrawl use).
* feat(approvals): typed approval scope summaries on channel cards
Approval owners can attach a closed ApprovalScope union (message-send,
payment, external-post) describing an action's blast radius. The gateway
sanitizes it once at the producer boundary, the core view model renders a
Scope metadata row so Slack/Discord/Google Chat cards show it unchanged,
shared text builders cover Telegram/WhatsApp/Signal/iMessage/Matrix, and
the durable presentation carries it additively for operator surfaces.
Scope is display-only, never authorization; missing scope keeps today's
cards.
* fix(approvals): emit native ApprovalScope union and clamp recipient previews
Name the three scope variants as registered protocol schemas so the Swift
generator emits the ApprovalScope discriminated union the presentation
structs reference, and commit the regenerated GatewayModels.swift. Clamp
recipient previews to the declared recipientCount at the sanitize boundary
so a count of 1 with 2 previews can no longer render inconsistently.
Addresses both ClawSweeper findings on #130116.
* refactor(approvals): extract text sanitizer to break the exec-approvals import cycle
check:architecture flagged approval-scope joining the exec-approvals SCC
through exec-approval-command-display. Move the self-contained display
sanitizer into a leaf module (exec-approval-text-sanitize) with no
exec-approvals imports and migrate all sanitize importers; command-display
keeps only the payload-typed command/preview resolver.
* chore(plugin-sdk): ratchet public surface budgets down after sanitizer extraction
The approval display sanitizers left the publicly reachable SDK graph when
they moved to the exec-approval-text-sanitize leaf: exports 4343 -> 4338,
callable exports 2582 -> 2578. Shrink-only budget pin.
* feat(a2a): add A2A v1.0 agent-interop channel plugin
Adds `a2a` as a bundled, default-off channel plugin so A2A-compliant agents
(Hermes Agent, LangChain, CrewAI, Google ADK) can discover an OpenClaw gateway
and send it tasks, and so OpenClaw can address configured peers.
Inbound HTTP (`/.well-known/agent-card.json`, `/a2a/v1`) runs through the normal
channel ingress path, so peer messages inherit allowlist admission, provenance
framing, and session routing. The agent reply returns through the channel
delivery callback and resolves the A2A task with its artifact, correlated per
(peer, contextId) FIFO so concurrent sends cannot cross-talk.
Wire format follows the canonical v1.0.0 spec sources (a2aproject/A2A
specification/a2a.proto): PascalCase SendMessage/GetTask/CancelTask,
supportedInterfaces[] rather than the 0.3-era top-level url/protocolVersion,
member-presence Part oneof, TASK_STATE_*/ROLE_* enums, no Task.kind. The 0.3
dotted method names are accepted as explicit compat aliases because shipped
Hermes-generation peers still send them.
Security: per-peer bearer tokens compared in constant time, per-peer sliding
window rate limit, 1 MiB body cap, 64 KiB inbound text cap, tasks scoped to
their owning peer, outbound redirect: "error", and no inbound-controlled target
URLs. With no peers configured the channel stays unconfigured and registers no
routes.
Live-verified against a real gateway on an isolated state dir with a mock
provider: 31/31 proofs, including the agent reply round-tripping into the task
artifact. That run caught two defects fixed here:
- The agent card read `cfg.agents.list` directly, so gateways configured with
the canonical `agents.entries` roster advertised zero skills. Now uses
listAgentIds/resolveAgentConfig, which read either roster shape.
- `returnImmediately` dispatches inherited the HTTP request's released work-
admission root and every async task failed as GatewayDrainingError. Now
reserves an independent root via runDetachedWebhookWork, matching sms/line/zalo.
* fix(a2a): route outbound sends through the SSRF guard and land setup metadata
CI on the first push surfaced four real defects that local `check:changed` did
not cover (it runs no oxlint or typecheck lane):
- Outbound peer sends used raw `fetch()`, tripping the channel/plugin
no-raw-fetch boundary. They now go through `fetchWithSsrFGuard` with
`maxRedirects: 0`, so A2A egress carries the same SSRF policy as every other
plugin call and a redirect cannot re-target a delivered task.
- The channel declared an empty setup contract while its package manifest
declared none, so the bundled-channel shape contract failed. The wizard now
collects `advertisedUrl` plus a first peer name/token pair, which is the
minimum that leaves A2A actually configured (it stays unconfigured until one
peer credential exists), and the package manifest mirrors that metadata.
- Six lint errors in the test files: base-to-string on `BodyInit` bodies, an
unused mock, and a shadowed `options` parameter. Request bodies now go through
a typed reader that asserts the serialized-string contract before parsing.
- A card assertion referenced `capabilities` without declaring it on the local
response type, failing test typecheck.
Outbound redirect handling moves from `redirect: "error"` to the guard's
`manual` inspection; the adapter tests assert the new shape.
* fix(a2a): drop route replaceExisting, accept configWrites, register runtime api
CI after the interop run surfaced four more gates:
- `registerPluginHttpRoute` used `replaceExisting: true`, tripping the
GHSA-RQP8-Q22P-5J9Q rule: a duplicate path can silently replace another
account's handler. A2A owns fixed global paths on a single account, so a
duplicate registration means a stale or conflicting owner. It now fails loudly
through `throwOnFailure` instead.
- The channel schema rejected `channels.a2a.configWrites`, which every bundled
channel must accept; added to the zod schema, the manifest schema, and the
config type, then regenerated the channel metadata.
- The new `input` assertion in the setup adapter needed a `// SAFETY:` line for
the assertion ratchet.
- `extensions/a2a/runtime-api.ts` needed registering in the runtime-api
classification list, like every other bundled channel barrel.
Also repairs a pre-existing incomplete `vi.mock` factory for `../infra/fs-safe.js`
in the agent-delete suites. Both pass in isolation, but the shared-worker lane
lets a sibling importer reach `readLocalFileSafely`, which the partial factory
never exported; adding this channel changed shard composition and surfaced it.
The factory now spreads the real module and overrides only `movePathToTrash`.
* chore(a2a): refresh config baseline for the configWrites surface
* chore(tests): adopt main's fs-safe mock repair for the agent-delete suites
Main landed the same importOriginal spread for the shared-worker mock-defeat
while this branch carried an equivalent fix; take main's version verbatim so the
two do not conflict.
* fix(a2a): isolate peer sessions and stop faking task cancellation
Addresses both ClawSweeper P1 findings.
Peer sessions: inbound routing never passed `dmScope`, so it fell back to
`session.dmScope ?? "main"` and every authenticated peer converged on
`agent:main:main` under default config. Untrusted remote content was joining the
operator's own session, and peers shared conversation history. A2A now pins
`per-account-channel-peer`; the peer id already embeds the A2A contextId, so each
peer+context pair gets its own session regardless of global session config.
Cancellation: `CancelTask` marked the task `TASK_STATE_CANCELED` and discarded
the late reply, but the dispatched agent run kept going and could still use
tools. The peer was told the work stopped when it had not. There is no
plugin-facing seam to abort a live run, so cancellation is refused with -32004
instead of acknowledged, and the store's cancel path plus its canceled-delivery
tombstone are deleted rather than left as a lying terminal state. Documented as
an explicit limitation.
Live-verified on a real gateway: 34/34 proofs, including three distinct
`agent:main:a2a:default:direct:<peer>:<context>` sessions with zero traffic in
`agent:main:main`, and both cancel spellings refused while the task stays
WORKING. Re-ran the official a2a-sdk 1.1.2 Docker interop afterwards: 9/9.
The detached Skill Workshop experience review rebuilt its system prompt and tool catalog from a different context than the foreground turn, so every review missed the prompt cache. Native harnesses (embedded, Codex, Copilot) now hand the review the same foreground prompt context via buildEmbeddedForegroundPromptContext; the review reuses the foreground prefix and gates execution to skill_workshop while keeping the catalog identical. Reviews without a foreground prompt (CLI hook contexts) are skipped.
* fix(cli-runner): drop stock watchdog defaults from CLI backend descriptors (#125045)
The claude-cli and gemini-cli backend descriptors spread
CLI_FRESH/RESUME_WATCHDOG_DEFAULTS into config.reliability.watchdog.
Those copies are byte-identical to the fallback pickWatchdogProfile
already uses, so shipping them only makes `configured` always truthy,
which permanently disables the promotion gate (!configured) and pins
resumed cron/explicit-timeout turns to the 180s resume no-output ceiling
instead of the 600s fresh ceiling.
Removing the blocks lets `configured` be undefined, restoring the
promotion gate. No user-config path is affected: reliability.watchdog
is not exposed in the config schema, the descriptor is the sole source,
and pickWatchdogProfile is the only runtime reader. The codex backend
never shipped the block, so this aligns the three backends.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(cli-runner): add descriptor-through-resolver watchdog proof (#125045)
Drive the real shipped Anthropic/Gemini CLI backend descriptor config through
the production resolveCliNoOutputTimeoutMs resolver to prove the resumed-cron
promotion is live post-fix. Pre-fix the descriptor shipped byte-identical
resume defaults, so configured was truthy and the !configured promotion gate
was dead, pinning resumed cron turns to the 180s resume ceiling (180000).
Post-fix the block is gone and the resolver returns the promoted fresh budget
(480000 = 600000 * 0.8).
resolveCliNoOutputTimeoutMs is re-exported through the test-only
openclaw/plugin-sdk/test-fixtures subpath (excluded from published dist), so
extension tests can drive the real descriptor through the real resolver without
crossing the extension/core boundary.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(anthropic): split watchdog promotion proof into its own file
Move the descriptor-shape and descriptor→resolver real-behavior proof
tests out of cli-shared.test.ts into cli-watchdog-promotion.test.ts.
cli-shared.test.ts exceeded the extensions max-lines budget (1000
non-blank/non-comment lines) once the proof tests landed; the focused
file keeps the same test names and coverage while staying under the
budget. No production change.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(cli-runner): restore inherited resume watchdog promotion
Validate bundled plugin descriptors through their public setup entries and keep custom watchdog contracts intact.
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
* chore(cli-runner): align conflicting provider owner with main
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
* fix(cli-runner): complete inherited resume watchdog promotion
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
* test(cli): keep watchdog regressions in their owning plugins
Keep Anthropic and Google descriptor assertions inside their plugin-owned registration suites, preserve generic resumed/custom watchdog coverage in its core owner, and remove the cross-plugin core test rejected by architecture CI.
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
---------
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* docs(copilot): describe the transcript journal, not the removed dual-write mirror
The new copilot plugin doc described transcript persistence through
extensions/copilot/src/dual-write-transcripts.ts with session-scoped
content-fingerprint keys and best-effort failure containment. That module
was removed in #114403 (Jul 27): attempts now own an
attempt-transcript-journal keyed by run id and SDK event id, journaled in
structurally complete groups, and persistence failures fail closed by
aborting the SDK session and flagging replay as unvalidated.
Rewrite the section around the current journal, and update the two
"transcript mirror" references that pointed at it.
* docs(copilot): clarify invalid replay starts a fresh session
Co-authored-by: Santhi Prakash <b.santhiprakash@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): preserve CLI session binding on format-class failover
shouldClearFailedCliSessionBinding cleared the stored CLI session
binding for every FailoverError, including format-class failures
(output limit exceeded, parser error, unsupported image input). A
format-class failover means the stream could not be read, not that
the CLI session on disk is invalid — clearing the binding silently
lost the conversation context on the next turn.
Narrow the condition to only session-invalidating failover reasons
(session_expired, auth, auth_permanent) using a whitelist so new
reasons default to preserving the binding.
Fixes#128698
* fix(agents): preserve valid CLI session bindings
* fix(agents): retain CLI binding across format recovery
* fix(agents): scope fresh CLI recovery by backend
* docs(plugins): document CLI recovery policy
---------
Co-authored-by: Vincent Koc <vincentkoc@users.noreply.github.com>
* feat(geolocation): resolve client addresses to a coarse city via a bundled plugin
The Activity identity card could show a client's IP address but not where it
was, so an operator still had to look the address up by hand.
Add a bundled `geolocation` plugin that owns address-to-place resolution behind
one authenticated route, `GET /plugins/geolocation/lookup?ip=`. It downloads a
MaxMind-format database on first lookup into the state directory, answers from
that local copy, and refreshes it monthly, so a lookup never sends an address
to a third party. The Control UI renders the resolved city on the device row
next to the address and the client-reported time zone.
The default source is DB-IP City Lite under CC BY 4.0. That license requires
attribution, so every response carries the credit and the UI renders it next to
the value; the database is downloaded at runtime and never redistributed.
Plugin code and the `maxmind` reader are MIT. No free city-level IP database is
MIT-licensed, so the obligation lives with the data rather than the code, and
`databaseUrl` plus the attribution fields make the source swappable.
No new core provider kind: with one implementation the plugin owns everything
through the existing HTTP-route seam, keeping core plugin-agnostic. A second
provider is what would justify promoting this to a registry contract.
Availability and lookup failure stay distinguishable: a missing or still
downloading database answers 503, never `found: false`. A failed refresh serves
the cached copy, and a body that does not parse as an MMDB is discarded without
replacing a working database.
* fix(docs): correct geolocation config examples and add zh-CN glossary entries
The config examples used `plugins.<id>` instead of the real
`plugins.entries.<id>.config` shape, which the docs config-example
validator and src/config/docs-config-examples.test.ts both reject.
New doc labels also need zh-CN glossary entries.
* chore(labeler): cover the geolocation extension directory
AGENTS.md requires a labeler entry plus a GitHub label for every new
plugin surface; test/scripts/labeler-extension-coverage.test.ts enforces
the labeler half.
* fix(geolocation): address review findings on caching, download bounds, and scope
Cold-start lookups were permanently suppressed. The loader cached one promise
per address including failures, so the 15s browser deadline expiring against a
first download that takes ~46s cached a blank forever, and a mounted row only
looks up again when its IP changes. Lookups now return a discriminated
located/absent/unavailable result: only definitive answers are cached, and the
element retries an unavailable one on a widening 5s/15s/45s schedule.
Download limits ran after allocation. The size check happened only after
`response.arrayBuffer()` had buffered the whole body, and gunzip had no output
ceiling, so a replaced source or a compression bomb could exhaust Gateway memory
before rejection. The body now streams against a compressed ceiling enforced
per chunk, and inflation uses zlib's maxOutputLength.
Cached placements were not scoped to the Gateway. The cache keyed only by
address while endpoint and credentials come from the shared Gateway context, so
a switch could render the previous Gateway's answer. The shared reset hook now
supports multiple subscribers - a single slot silently dropped whichever
registered first - and the geolocation cache subscribes.
Unresolvable ranges no longer trigger a download. Only loopback suppresses `ip`
at connect, so Tailscale carrier-grade-NAT and LAN addresses are recorded and
displayed. No geolocation database contains them, so a tailnet-only or LAN-only
Gateway was downloading 125 MB to answer nothing. The route now answers those
ranges without loading the database, using the already-public
`isPrivateOrLoopbackHost` seam so the SDK surface budget is unchanged.
The quickstart queried a reserved documentation range while showing a located
response, which cannot happen; it now uses a routable address and documents the
not-found case.
* fix(deps): resync the lockfile after dropping the net-policy dependency
The geolocation plugin briefly depended on @openclaw/net-policy before
switching to the already-public isPrivateOrLoopbackHost SDK seam. The
package.json entry was removed without regenerating the lockfile, so the
frozen-lockfile install failed and every downstream CI job failed with it.
* refactor(anthropic): explore official Claude Agent SDK runtime
* refactor(anthropic): replace handwritten Claude sessions with SDK
* refactor(anthropic): collapse SDK live-session ownership
* refactor(anthropic): simplify SDK ownership and preserve live skills
* fix(anthropic): fence cancelled SDK runs before process startup
* fix(anthropic): harden SDK approvals, lifecycle, and packaging
* refactor(anthropic): own SDK process trees and streamline runtime
* fix(anthropic): repair rebased packaging and legacy test fixtures
Remove the bundled OpenProse plugin and /prose command now that upstream owns the maintained Agent Skill. Preserve /prose as migration documentation and let Doctor clean stale plugin configuration.
BREAKING CHANGE: The bundled OpenProse plugin and /prose command are removed.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Adds a generic plugin-declared selectable-context-window surface mirroring thinkingLevels: ModelCatalogEntry.contextWindows + contextWindowDefault through catalog normalization and the gateway protocol, session validation on sessions.create/patch, and a 200K/1M switch inside the Control UI model picker for Claude CLI 5-series models. The Anthropic plugin owns the option mapping: explicit 1m → `[1m]` argv suffix, 200k → bare id + CLAUDE_CODE_DISABLE_1M_CONTEXT=1, omitted → bare id (shipped default argv). Run budgets follow the selection on both CLI and native paths, so a 200K session gets a matching auto-compact window instead of a silent 1M budget.
Review fixes landed in this PR: run-owner prepared-fact plumbing so ordinary replies honor the selection; atomic catalog overlay merge and normalization for the options/default tuple; one-owner tuple reads in the picker; sessions.create key-presence patch semantics; native-run budget capping.
Feature direction and in-picker switch shape by @obviyus (maintainer review).
Keep plugin-originated text and media bound to the active host-selected route and revoke authority at turn closure.
Fence Gateway-owned channels until a server-verifiable authority and media-policy contract exists.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>