Queue Telegram-visible Mantis proofs at workflow level so only one shared-user run allocates a runner at a time.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* 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.
* feat(control-ui): show client IP and time zone on the activity identity card
The Activity identity card showed only host and platform, so an operator
looking at a teammate could not tell where that person was connecting from.
Presence already carried a best-effort `ip`, and it was simply not rendered.
Add the client's self-reported IANA time zone to the connect handshake and
presence entry, and render both `ip` and `timeZone` on the device row. The
time zone matters because the connecting address is frequently unusable for
location: connect handling omits `ip` for loopback clients, and tunneled or
Tailscale clients land in private/CGNAT ranges. A browser knows its own zone
regardless of how it reached the gateway.
Both protocol additions are optional fields, so no version bump is needed.
* build(protocol): regenerate Swift models for presence timeZone
* docs: fix five broken cross-page anchor links
Five internal doc links pointed at anchors that do not exist on the target
page, so readers following them landed at the top of the page instead of the
referenced section.
- /gateway/cloud-workers#desktop -> #desktop-interactive
(heading is "## Desktop (interactive)"), from experimental-features and
the gateway protocol reference.
- /web/urls#special-documents-and-startup-modes ->
#other-special-documents-and-startup-modes
(heading is "## Other special documents and startup modes"), twice from
the Control UI page.
- /plugins/sdk-runtime#api-runtime-agent -> #runtime-namespaces
"api.runtime.agent" is an <Accordion> title, not a heading, so it emits no
anchor; the enclosing "## Runtime namespaces" heading is the real target.
Sibling links on all three target pages already use heading-derived anchors
(#coordinator-backed-crabbox, #route-table, #reusable-runtime-utilities),
and this was the only link in docs/** aiming at an Accordion title out of
1342 such titles.
Docs-only change; no runtime or config surface is touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: restore the sdk-entrypoints deep link
Running the real Mintlify anchor audit (pnpm docs:check-links:anchors) showed
that /plugins/sdk-runtime#api-runtime-agent already resolves: Mintlify does
generate anchors from <Accordion title="..."> elements, so the original link
was correct.
The audit flags fragments it cannot resolve -- it reported both
cloud-workers#desktop and urls#special-documents-and-startup-modes on main --
and it never reported this one. Retargeting it to #runtime-namespaces would
have replaced a precise deep link with a coarser section link, so this restores
the original.
The remaining four link instances in this PR are confirmed broken by the same
audit: main reports 313 broken links across 45 files, this branch reports 309
across 42, with no newly broken links.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`agents.defaults.modelPolicy.allow` seeded from a legacy model map kept
`claude-cli/*` refs that doctor never rewrote, while the model map migrated
to canonical `anthropic/*` keys — leaving no selectable Claude model after
upgrade. `openclaw doctor --fix` now canonicalizes explicit allowlists in
the same pass: legacy refs migrate to provider refs with their claude-cli
runtime policy preserved, and superseded legacy model-map keys are dropped.
Closes#124952
Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(heartbeat): keep background runs off the main lane
* docs(queue): describe heartbeat background lane
* fix(agents): forward heartbeat lane to embedded fallback
* test(agents): prove heartbeat and visible lane overlap
* test(agents): expose heartbeat lane in embedded harnesses
* refactor(agents): keep heartbeat lane choice at fallback boundary
Signed-off-by: sallyom <somalley@redhat.com>
---------
Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
The Telegram Desktop proof previously built the candidate from the raw PR
head, so a head behind main failed for reasons main already fixed
(observed on #127770: 59 commits behind, hitting the Unknown-model defect
fixed by #127952). GitHub's cached PR test merge cannot be the candidate
either: it was observed 50 commits / 12 hours stale and never refreshed.
The workflow now resolves the live refs/heads/main tip and merge base at
dispatch, requires a main-targeting PR, and builds a deterministic local
merge (merge-tree --merge-base + commit-tree with pinned identity/date) as
the candidate; conflicts fail with a direct rebase message and there is no
fallback to the raw head. Lane labels and docs now say the candidate is
the PR merged onto main.
* refactor(mantis): reuse authorized desktop captures
* fix(mantis): budget desktop authorization failures
* chore(mantis): bound desktop proof retries
* fix(e2e): drop unused recorder failure fact type export
* fix(ci): route Mantis desktop teardown through the recorder wrapper
Cleanup invoked the internal recorder executable as mantis-sut, which is
deliberately kept out of the docker group and cannot read the
recorder-owned session file; teardown therefore failed and blocked
safe_to_release. The cleanup step already runs as the recorder user, so
call the public wrapper whose exec shim cds into the session root.
* fix(e2e): make recorder failure fact lane-readable; document v2 lifecycle
The Mantis workflow runs the recorder as the desktop user while the lane
reads the authorization-failure fact as mantis-sut; 0600 made that read
fail EACCES and silently disabled the two-attempt retry budget. Write the
fact 0644 — the 0770 attempt directory bounds visibility.
Update the mantis doc's recorder section for the v2 session lifecycle:
required --session handle with healthy-session reuse, capture-only stop,
and teardown owning authorization termination and lease release.
* feat(control-ui): stream live draft previews in the typing indicator
Multi-identity sessions now show what a teammate is typing, not just that
they are typing: the composer's per-keystroke session.typing sends carry a
bounded tail of the draft (optional preview field, 400 code points max),
the gateway throttle re-emits on changed payloads at 250ms (boolean-only
stays at 1s, trailing edge keeps the latest draft), and the transcript
renders a per-actor bubble with the live text plus a blinking caret.
Actors without preview data keep the three-dot bubble.
Previews are ephemeral presence: never persisted, never part of the
session transcript or model context, excluded from aria-live regions, and
gated by the existing >=2-live-viewers, sharing-role, and incognito
checks. No new config surface.
* chore(protocol): regenerate Swift gateway models for typing preview
* fix(gateway): aggregate typing previews across same-actor connections
A boolean-only session.typing update from a second connection of the same
actor (another tab or device) erased their live draft preview, because
typing liveness aggregated per actor while the broadcast preview came only
from the latest request. Preview aggregation now lives with the connection
aggregation owner: updateTypingConnections tracks per-connection previews
and returns the newest non-empty preview among live connections, so the
broadcast keeps the active draft until its connection stops or expires.
Regression fails pre-fix (event lost its preview field).
Give maintainers immediate visibility when Mantis is requested. Bare mentions now react, link the active run, and keep one run-owned status comment through proof, short-circuit, or failure.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Add trusted ClawSweeper-label and maintainer-comment dispatch for Mantis Telegram proof. Short-circuit non-visible PRs before desktop setup while preserving exact-head, fork, credential, and comment-ownership boundaries.
Move Mantis Telegram Desktop proof from the remote AWS/Crabbox lane to a recorder-driven local Docker desktop. Keep proof scenarios agent-authored, cache trusted build outputs, and publish exact visible Telegram evidence without writing the QA bot token to artifacts.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(auth): keep a retired auth JSON from stranding a migrated store
Runtime failed closed with AUTH_PROFILE_MIGRATION_REQUIRED whenever a retired
credential file was present, even when the canonical SQLite store already held
the agent's profiles. One leftover auth.json therefore made a fully migrated
install unusable, and the gateway lifecycle preflight refused start/restart on
top of it, so every channel and provider stayed offline until Doctor ran.
A legacy file is now only fatal when the canonical store cannot serve
credentials. Doctor's importer never overwrites a usable stored credential, so
a file sitting beside a populated store is unarchived bytes, not pending
migration: runtime logs a one-time warning and keeps serving. An empty store
with a credential file still fails closed and never falls through to
environment auth. Startup degrades that owner to configured-unavailable
instead of refusing to boot, which lets the lifecycle preflight go away.
* refactor(secrets): retire the auth-profiles.json vocabulary
Auth profiles moved to SQLite, but operator-facing surfaces still named the
retired JSON file. The duplicate-agentDir error told operators to copy
auth-profiles.json to share credentials, which does nothing and lands the
second agent in a migration-required state; `openclaw migrate plan codex`
reported a target file that is never created; and the secrets picker labelled
candidates with a filename that no longer exists.
Renames the SecretTargetConfigFile discriminator to "auth-profile-store" and
corrects the operator-facing text, the migrate plan target, and the docs that
described the file as a live target. Genuine legacy-filename uses in doctor,
the security fixer, and migration fixtures are unchanged.
Also deletes resolveSecretPlanTargetByPath and ResolvedSecretPlanTarget from
the plugin SDK. They have no callers in core, plugins, or tests, and the
symbols are absent from the latest stable tag, so they carry no compatibility
obligation and are removed rather than deprecated. Their inline parameter type
was the only thing putting the retired filename on the public SDK surface.
* improve(wizard): warn about device-code phishing
The device-code prompt only warned against sharing the code, and only when an
expiry was known. Device-code phishing works the other way around: the attacker
starts the login and gets the victim to enter the attacker's code. Codes
delivered over a chat channel are the risky case and carry no expiry hint, so
the warning is now unconditional and covers received codes, matching the Codex
CLI prompt.
Also documents the Codex auth handoff: a subscription profile is installed as
in-memory external auth rather than persisted, and token refresh is inverted
so the refresh token stays in OpenClaw's store.
* fix(test): make transcript read-failure injection order-independent
server.sessions.compaction-read-errors.test.ts injected its failures with
mockRejectedValueOnce, which fails the NEXT call to loadTranscriptEvents
globally. Under --isolate=false a shard shares one worker, so any sibling
transcript read could consume the one-shot rejection before the compaction RPC
issued its own; compaction then ran against the real reader and returned ok,
failing three assertions. This shard was already red on main; a prior repair
fixed the mock's initialization order but left the call-order dependency.
Key the injection on the seeded sessionId instead, so unrelated readers cannot
consume it and the re-read case counts only its own session's reads.
Also updates two expectations invalidated by this branch: the duplicate-agentDir
remediation text, and the plugin SDK export ratchet, shrunk by the two retired
secret-plan exports.
* fix: capture GitHub identity from authenticated sign-in
Automatically persist verified GitHub identities from Cloudflare Access and Tailscale Serve while keeping public Git co-author credit as a separate opt-in.
* test: stabilize cleanup and activity capture
* fix(security): bind GitHub profiles by account id
* test: scope activity capture to route
* fix(security): gate profile requests on identity sync
* fix(security): close pending profile authorization gaps
* test(ui): stabilize terminal continuation menu
* test: stabilize startup recovery timing
* test: keep one Codex attempt tools owner
* fix(plugins): allow profile-independent gateway reads
Records native Telegram Desktop from a digest-pinned prebaked image: a ready desktop in ~12s with no per-lease apt installs or downloads, which also removes the dpkg-lock failures that killed recent runs.
The recorder only records; callers drive the turn and supply the TDLib driver used for QR authorization. Nothing in this repository invokes it yet - routing the Mantis Telegram Desktop Proof workflow through it is a follow-up.
* feat: credit linked session participants as co-authors
Authenticated profiles can link GitHub and receive automatic co-author credit in shared coding sessions.
* style: format rebased co-author registries
* fix: mark profile schema DDL boundary
* feat(agents): default to eager delegation in the main session
The delegation prompt section now defaults to "prefer" in each agent's
canonical main session and "suggest" elsewhere; explicit config wins in
both directions. The section is rewritten to be token-leaner and now
covers the hidden-vs-visible spawn distinction (hidden subagents are
invisible and auto-archived; deliverable-bearing work spawns
visible=true and replies with the link) plus the run-end notification
contract. The Messaging spawn-mechanics line is suppressed when the
Delegation section renders, and the stable Tooling visible:true hint is
aligned with the new guidance.
* perf(sessions): coalesce session-state wake bursts
Watched-session change notices woke the watcher's main session with the
generic 250ms heartbeat coalesce, so a burst of changes across several
watched sessions produced one wake per change. Wakes now coalesce for
20s; notices are already queued and deduped, so none are lost.
* test(agents): regenerate prompt snapshots after rebase
* test(sessions,agents): update sibling assertions for coalesced wakes and visible guidance
session-state-events tests advance timers past the new 20s wake
coalesce window; the sessions_spawn schema test tracks the updated
visible description.
Keep ambient main-session group notices working when direct messages use an isolated dmScope, while preserving explicit session watches and routed group topology.
* feat(agents): unify agent status into a durable progress_card
Replace the write-only update_plan to-do tool and the fragmented plan
rendering with one durable status artifact per session: progress_card
({plan?, markdown?}, replace-on-write, 8 KiB markdown / 50-step caps).
Cards persist in a lazy-additive session_progress_cards table in the
per-agent DB (no schema-version bump), broadcast progressCard.changed,
and render from the store with exactly one live placement per view
(session rail when visible, else the composer-adjacent bar); transcripts
collapse to one-line receipts, and the sidebar hovercard shows other
sessions' cards inline (markdown + <progress>, DOMPurify allowlist, no
iframes). The three stream-derived plan renderers and their dedup
heuristics are deleted.
Codex runs disable the native plan tool per thread
(tools.update_plan.enabled=false) and receive progress_card via the
dynamic-tool bridge; compaction restore now reinjects the card (steps +
bounded markdown). Card writes still emit the legacy plan stream event so
native apps and channels keep working until their per-platform
migrations. Policy names map update_plan -> progress_card; the shipped
tools.updatePlan=false kill switch is honored.
Net -277 production LOC; -480 test LOC.
* test(agents): regenerate Codex prompt snapshots for update_plan thread-config disable
* chore(protocol): allowlist progressCard.changed for native apps pending card migration
* fix(ci): repair progress card integration checks
* fix(codex): canonicalize native progress cards
* test(gateway): reconcile progress card method order
* test(codex): stabilize native approval fixture
* docs: document three-layer session ownership
User documentation for the session-ownership feature landed in #125057:
the immutable creator / assignable owner / participant-history model, the
Assign-to-me and Assign-to session menu actions, the sessions tool
assign_owner action, the sidebar Owners facet with Involving-me, the
pair-stack owner avatar, and agent-spawned session receipts (sessionUrl +
owner acknowledgement). Adds the sessions.assignOwner method and ownership
row projections to the protocol reference and a zh-CN glossary entry for
the new link label.
* docs: drop nonexistent header facepile overflow count
The chat header receives the already 4-capped participant projection and no
total count, so its overflow branch cannot render. Describe up to four
avatars instead (ClawSweeper P2).
Adds an additive agent_provenance table (shared state DB, schema v8) owned
by src/state/agent-provenance.ts. createAgent() records operator/agent
provenance after commit, the system-agent create-agent operation passes its
own id as creator, and Claw installs record created-via claw at their roster
commit point. Agent deletion removes the agent's own row inside the deletion
journal transaction; children keep dangling creator ids as historical fact.
openclaw agents list gains --tree (provenance hierarchy) and JSON provenance
fields.
* feat(protocol): add named session icon glyphs
* feat(ui): add session glyph and custom emoji picker
* docs: describe named and custom session icons
* fix(ui): give the custom emoji input an accessible name
ClawSweeper P2: the input had no label relationship; assistive tech announced an unnamed edit control.
* fix(protocol): guard the v-flag icon regex for browser module loads
ClawSweeper P1: the picker's static import evaluates this module in the browser; pre-Unicode-Sets engines threw at module scope and took down the session menu. Lazy capability-guarded construction; such engines fall back to the grapheme heuristic as client pre-validation while the Gateway keeps exact RGI validation.