Files
Peter Steinberger 295809df98 feat(geolocation): resolve client addresses to a coarse city via a bundled plugin (#128546)
* 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.
2026-08-24 04:56:03 -07:00
..

OpenClaw docs i18n assets

This folder stores translation config for the source docs repo.

Generated locale trees and live translation memory now live in the publish repo:

  • repo: openclaw/docs
  • local checkout: ~/Projects/openclaw-docs

Source of truth

  • English docs are authored in openclaw/openclaw.
  • The source docs tree lives under docs/.
  • The source repo no longer keeps committed generated locale trees such as docs/zh-CN/**, docs/zh-TW/**, docs/ja-JP/**, docs/es/**, docs/pt-BR/**, docs/ko/**, docs/de/**, docs/fr/**, docs/hi/**, docs/ar/**, docs/it/**, docs/vi/**, docs/nl/**, docs/fa/**, docs/ru/**, docs/tr/**, docs/uk/**, docs/id/**, docs/pl/**, or docs/th/**.

End-to-end flow

  1. Edit English docs in openclaw/openclaw.
  2. Push to main.
  3. openclaw/openclaw/.github/workflows/docs-sync-publish.yml mirrors the docs tree into openclaw/docs.
  4. The sync script rewrites the publish docs/docs.json so the generated locale picker blocks exist there even though they are no longer committed in the source repo.
  5. openclaw/docs/.github/workflows/translate-all.yml waits for main to settle, translates only stale or missing locale pages, and uploads per-locale artifacts.
  6. The publish repo finalizer applies successful locale artifacts and pushes one aggregate chore(i18n): refresh translations commit.
  7. A weekly full run reconciles every locale/page path so flaky model failures are retried without making hot docs commits wait.

Why the split exists

  • Keep generated locale output out of the main product repo.
  • Keep Mintlify on a single published docs tree.
  • Preserve the built-in language switcher for Mintlify-supported generated locales by letting the publish repo own generated locale trees.
  • Keep generated Thai (th) and Persian (fa) docs plus translation memory even though Mintlify does not currently accept those codes in navigation.languages. Their absence from the built-in docs language picker is a host limitation, not a failed translation run.

Locale visibility

  • Control UI supports en, zh-CN, zh-TW, pt-BR, de, es, ja-JP, ko, fr, hi, ar, it, vi, nl, fa, ru, tr, uk, id, pl, and th.
  • Docs translation workflows generate the same non-English locale set in openclaw/docs.
  • The Mintlify docs language picker can expose only the locales accepted by Mintlify navigation.languages; Russian (ru) and Hindi (hi) are now included in the publish configuration.
  • Do not treat locale visibility in generated docs/docs.json as proof that translation artifacts exist. Verify each generated locale folder and its translation memory in openclaw/docs.

Files in this folder

  • glossary.<lang>.json — preferred term mappings used as prompt guidance.
  • zh-Hans-navigation.json — curated zh-Hans tab and group labels overlaid onto the current English navigation tree during publish sync.
  • ar-navigation.json, de-navigation.json, es-navigation.json, fr-navigation.json, id-navigation.json, it-navigation.json, ja-navigation.json, ko-navigation.json, pl-navigation.json, pt-BR-navigation.json, and tr-navigation.json — starter locale labels kept alongside the source repo. Publish sync clones the full English navigation tree, prefixes locale routes, and overlays translated labels by matching shared page anchors.
  • <lang>.tm.jsonl — translation memory keyed by workflow + model + text hash.

In this repo, generated locale TM files such as docs/.i18n/zh-CN.tm.jsonl, docs/.i18n/zh-TW.tm.jsonl, docs/.i18n/ja-JP.tm.jsonl, docs/.i18n/es.tm.jsonl, docs/.i18n/pt-BR.tm.jsonl, docs/.i18n/ko.tm.jsonl, docs/.i18n/de.tm.jsonl, docs/.i18n/fr.tm.jsonl, docs/.i18n/ar.tm.jsonl, docs/.i18n/it.tm.jsonl, docs/.i18n/vi.tm.jsonl, docs/.i18n/nl.tm.jsonl, docs/.i18n/fa.tm.jsonl, docs/.i18n/tr.tm.jsonl, docs/.i18n/uk.tm.jsonl, docs/.i18n/id.tm.jsonl, docs/.i18n/pl.tm.jsonl, and docs/.i18n/th.tm.jsonl are intentionally no longer committed.

Glossary format

glossary.<lang>.json is an array of entries:

{
  "source": "troubleshooting",
  "target": "故障排除"
}

Fields:

  • source: English (or source) phrase to prefer.
  • target: preferred translation output.

Translation mechanics

  • scripts/docs-i18n still owns translation generation.
  • Doc mode writes x-i18n.source_hash into each translated page.
  • The publish workflow precomputes a pending file list by comparing the current English source hash to the stored locale x-i18n.source_hash.
  • If the pending count is 0, the expensive translation step is skipped entirely.
  • If there are pending files, the workflow translates only those files.
  • Locale workers retry transient model-format failures, but unchanged files stay skipped because the same hash check runs on each retry.
  • Locale workers upload artifacts; the publish repo finalizer commits all successful locale outputs together.
  • Published GitHub releases dispatch one aggregate translation refresh so release docs can catch up without waiting for the weekly reconciliation.

Operational notes

  • Sync metadata is written to .openclaw-sync/source.json in the publish repo.
  • Source repo secret: OPENCLAW_DOCS_SYNC_TOKEN
  • Publish repo secret: OPENCLAW_DOCS_I18N_OPENAI_API_KEY
  • If locale output looks stale, check the Translate All workflow in openclaw/docs first.