@flame0510/project-aether 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +3 -1
  2. package/agent-templates/README.md +42 -22
  3. package/agent-templates/base-image/Dockerfile +42 -33
  4. package/agent-templates/base-image/entrypoint.sh +67 -12
  5. package/app/agents/BrowserAccessSection.tsx +510 -0
  6. package/app/agents/ChannelManager.tsx +19 -11
  7. package/app/agents/ImageDownloadBanner.tsx +53 -19
  8. package/app/agents/ModelSection.tsx +316 -0
  9. package/app/agents/PageClient.tsx +708 -167
  10. package/app/agents/UpdateSection.tsx +300 -0
  11. package/app/agents/create/PageClient.tsx +11 -49
  12. package/app/agents/create/page.tsx +8 -21
  13. package/app/api/agents/[id]/backup/route.ts +26 -69
  14. package/app/api/agents/[id]/channels/pairing/route.ts +3 -3
  15. package/app/api/agents/[id]/channels/telegram/route.ts +2 -2
  16. package/app/api/agents/[id]/cold-backup/route.ts +56 -0
  17. package/app/api/agents/[id]/devices/route.ts +126 -0
  18. package/app/api/agents/[id]/invite-link/route.ts +53 -0
  19. package/app/api/agents/[id]/lifecycle/route.ts +3 -0
  20. package/app/api/agents/[id]/model/route.ts +113 -0
  21. package/app/api/agents/[id]/open-control-ui/route.ts +58 -0
  22. package/app/api/agents/[id]/recreate/route.ts +33 -187
  23. package/app/api/agents/[id]/restart/route.ts +5 -0
  24. package/app/api/agents/[id]/restore/route.ts +40 -70
  25. package/app/api/agents/[id]/route.ts +38 -169
  26. package/app/api/agents/[id]/update/rollback/route.ts +30 -0
  27. package/app/api/agents/[id]/update/route.ts +50 -0
  28. package/app/api/agents/activity-summary/route.ts +67 -0
  29. package/app/api/agents/create/route.ts +91 -145
  30. package/app/api/agents/devices-summary/route.ts +37 -0
  31. package/app/api/agents/download-image/route.ts +16 -9
  32. package/app/api/agents/image-status/route.ts +31 -111
  33. package/app/api/agents/models-summary/route.ts +163 -0
  34. package/app/api/agents/route.ts +25 -49
  35. package/app/api/agents/token/route.ts +33 -10
  36. package/app/api/assistant/route.ts +37 -16
  37. package/app/api/gateway/agent/route.ts +37 -6
  38. package/app/api/gateway/provider/balance/route.ts +5 -2
  39. package/app/api/gateway/provider/keys.ts +13 -1
  40. package/app/api/gateway/provider/route.ts +43 -12
  41. package/app/api/gateway/sync.ts +335 -76
  42. package/app/api/models/route.ts +28 -34
  43. package/app/api/provider/auth.ts +65 -0
  44. package/app/api/provider/upstream.ts +9 -2
  45. package/app/api/provider/v1/chat/completions/route.ts +22 -16
  46. package/app/api/provider/v1/models/route.ts +26 -133
  47. package/app/api/setup/agent-image/route.ts +14 -42
  48. package/app/components/DashboardToolbar.tsx +1 -1
  49. package/app/components/PulseChat.tsx +25 -39
  50. package/app/components/ui/RemoveButton.tsx +46 -0
  51. package/app/components/ui/Select.tsx +3 -2
  52. package/app/components/ui/index.ts +1 -0
  53. package/app/credentials/PageClient.tsx +2 -2
  54. package/app/gateway/PageClient.tsx +253 -674
  55. package/app/globals.css +8 -0
  56. package/app/lib/models-context.tsx +43 -7
  57. package/app/wizard/useWizard.ts +6 -1
  58. package/bin/rev4a.js +116 -50
  59. package/daemon.js +6 -6
  60. package/docs/ARCHITECTURE.md +110 -12
  61. package/docs/FRONTEND-ARCHITECTURE.md +31 -2
  62. package/docs/REV4A.md +93 -33
  63. package/docs/dev/API-REFERENCE.md +723 -178
  64. package/docs/dev/DATABASE.md +96 -0
  65. package/docs/dev/GATEWAY.md +250 -93
  66. package/docs/dev/PROVIDERS.md +26 -13
  67. package/docs/rag/DATA-FRESHNESS.md +59 -28
  68. package/docs/rag/GLOSSARY.md +27 -16
  69. package/docs/rag/REV4A-OVERVIEW.md +37 -25
  70. package/docs/rag/WHAT-I-CAN-ANSWER.md +10 -8
  71. package/instrumentation.ts +52 -1
  72. package/lib/agent-busy.ts +21 -0
  73. package/lib/agent-devices.ts +361 -0
  74. package/lib/agent-edit-state.ts +108 -0
  75. package/lib/agent-edit.ts +157 -0
  76. package/lib/agent-images.ts +375 -0
  77. package/lib/agent-ports-server.ts +27 -0
  78. package/lib/agent-ports.ts +68 -0
  79. package/lib/agent-readiness.ts +110 -0
  80. package/lib/agent-recreate-state.ts +108 -0
  81. package/lib/agent-recreate.ts +305 -0
  82. package/lib/agent-restore-state.ts +107 -0
  83. package/lib/agent-restore.ts +135 -0
  84. package/lib/agent-setup.ts +66 -17
  85. package/lib/agent-update-state.ts +122 -0
  86. package/lib/agent-update.ts +448 -0
  87. package/lib/agent-versions.json +14 -0
  88. package/lib/agent-versions.ts +80 -0
  89. package/lib/buildAgentImage.ts +88 -290
  90. package/lib/channelManager.ts +153 -64
  91. package/lib/cold-backup.ts +354 -0
  92. package/lib/container-file.ts +27 -0
  93. package/lib/credentials/delivery.ts +3 -3
  94. package/lib/db-bootstrap.mjs +76 -0
  95. package/lib/docker-utils.ts +3 -3
  96. package/lib/model-catalogue.ts +140 -27
  97. package/lib/provider-balance.ts +33 -12
  98. package/lib/rev4a-paths.ts +0 -21
  99. package/model-pricing.json +118 -110
  100. package/models.config.json +27 -12
  101. package/package.json +1 -1
  102. package/app/api/gateway/route.ts +0 -191
@@ -1,6 +1,6 @@
1
1
  # Rev4a Frontend Architecture
2
2
 
3
- > **Last updated:** 2026-08-28
3
+ > **Last updated:** 2026-09-15
4
4
 
5
5
  ## Layering
6
6
 
@@ -24,7 +24,8 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
24
24
  |-----------|------|---------|
25
25
  | `Button` | `Button.tsx` | Action button, 5 variants (`primary`, `secondary`, `danger`, `ghost`, `success`), 2 sizes (`sm`, `md`), loading spinner. |
26
26
  | `Input` | `Input.tsx` | Text input with label, error state, placeholder. |
27
- | `Select` | `Select.tsx` | Native select with typed options, label, error state. |
27
+ | `Select` | `Select.tsx` | Native select with typed options, label, error state. An option can be `disabled`, to display a current value that may not be chosen again. |
28
+ | `RemoveButton` | `RemoveButton.tsx` | The `×` that removes an item from a list — one `danger` style everywhere. Whether the removal is immediate or pending goes in `title`, not in the colour. Not for dismissing dialogs — see rule 8. |
28
29
  | `Modal` | `Modal.tsx` | Overlay modal, Escape-to-close, maxWidth prop, `type="button"` on close. |
29
30
  | `Toast` | `Toast.tsx` | Lightweight toast notification with auto-dismiss (4s), `success` / `error` variants. |
30
31
  | `LoadingSpinner` | `LoadingSpinner.tsx` | Inline or fullscreen spinner. |
@@ -41,6 +42,15 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
41
42
  | `Page` / `PageHeader` | `Page.tsx` | Full-page layout shell. |
42
43
  | `Icons` | `Icons.tsx` | SVG icons (`EyeIcon`, `EyeOffIcon`), 16/20px shared. |
43
44
  | `PasswordInput` | `PasswordInput.tsx` | Password input with inline show/hide toggle (`<button type="button">` with `aria-label`). |
45
+ | `UpdateSection` | `app/agents/UpdateSection.tsx` | OPENCLAW VERSION section of the agent detail panel: the version the agent runs, **Update to <version>** when a newer supported version is downloaded (confirm modal, disabled while the agent is stopped), the running update's steps with backup progress, the outcome, and **Roll back to <version>** after an update. Polls `/api/agents/[id]/update` every 2 s while an update or rollback runs; one action at a time (keyed busy state). |
46
+ | `BackupSection` | inline in `app/agents/PageClient.tsx` | BACKUP section of the agent detail panel, on the cold backup and the restore. **Backup Now** starts `POST /api/agents/[id]/cold-backup`; while the job runs a banner shows the file and its live percent with **Cancel** (`DELETE /cold-backup`). **Restore** (after a confirm) starts `POST /restore` and a banner shows `Restoring <file>…` (no percent: the extract is a single `tar xzf`, and there is no Cancel). Both sections poll their `GET` every 2 s while running, and on mount pick up a job that is already running — a backup lives in a Docker helper, a restore in `agent_restores`, so reloading the page or navigating away never loses them nor allows a second one (the server answers 409 anyway). Delete per row; all actions disabled while one runs; keyed busy state `{ kind, file }` so only the row in action shows the spinner. On the agent list, an activity Badge (fed by `/api/agents/activity-summary`, polled at 2 s only while something runs, otherwise riding the 15 s list poll) reads `BACKUP nn%`, `RESTORING`, `RECREATING`, `EDITING` or `UPDATING`. |
47
+ | `RecreateSection` | inline in `app/agents/PageClient.tsx` | RECREATE section of the agent detail panel. **Recreate Container** starts `POST /api/agents/[id]/recreate` (202) after a confirm; a banner then shows the phase — *Backing up … nn%* while the cold backup runs, *Recreating container…* while the container is rebuilt and the gateway starts. The section polls `GET /recreate` every 2 s, and on mount picks up a recreate that is already running, so a reload or navigation never loses it; it refetches the agent once the job reports `done`. |
48
+ | `ImageDownloadBanner` | `app/agents/ImageDownloadBanner.tsx` | Agent image banner on the Agents page. Polls `/api/agents/image-status` every 2 s; offers **Download Image** when no supported version is downloaded, **Download <version>** when the registry publishes a newer one, and shows the download in progress and its completion. Downloading changes no agent. |
49
+ | `BrowserAccessSection` / `OpenControlUiButton` | `app/agents/BrowserAccessSection.tsx` | Browser access to one agent's Control UI, in its detail panel: requests waiting for approval (Approve / Reject) and approved browsers (Rename / Revoke), refreshed every 5 s while mounted. A successful approve, reject, rename or revoke updates the list at once, since the refresh behind it runs the OpenClaw CLI and takes seconds; a read started before the mutation is discarded. On agents that require approval, "Invite link" fetches `/api/agents/[id]/invite-link` and shows the link in a read-only field with Copy, which uses the Clipboard API in a secure context and the field's selection over plain HTTP, plus a warning when the link uses localhost. `OpenControlUiButton` opens `/api/agents/[id]/open-control-ui` in a new tab inside the click; that route redirects to a one-time link that pairs the browser with no approval, or to the plain token link when none can be issued. Used on the agent cards and in the panel. |
50
+ | `ChannelManager` / `ChannelSection` | `app/agents/ChannelManager.tsx`, `ChannelSection` inline in `app/agents/PageClient.tsx` | Telegram, in the agent detail panel (`ChannelSection` is the card that opens the modal; the modal title is the agent's display name). Reads `GET /channels`; lists pending pairing requests with **Approve** (`POST /channels/pairing`) and approved senders with **Revoke** after a confirm (`DELETE /channels/pairing?senderId=`). Pending comes from `openclaw pairing list`, approved from OpenClaw's pairing store (`lib/channelManager.ts`). Polls pairings every 5 s while open; one keyed busy state per action. |
51
+ | `EditAgentModal` / `EditBanner` | `app/agents/PageClient.tsx` | Rename/ports editing. The modal has **Display Name** and **Host Port** (same input and validation messages as the create wizard, from `lib/agent-ports.ts`), sends `PATCH /api/agents/[id]` and closes on `202`; `EditBanner` (top of the agent detail) polls `GET /api/agents/[id]` every 2 s while an edit is `rebuilding`, resumes on mount, and refetches the agent once it finishes, so a reload or a navigation shows the running edit instead of allowing a second (the server answers 409). No backup is taken: the volume is untouched. |
52
+ | `ModelSection` | `app/agents/ModelSection.tsx` | Primary model and fallbacks for one agent, in its detail panel. Explicit save, no restart. The model is a property of the agent, not of the gateway. Tags models the catalogue marks `deprecated`. |
53
+ | `ModelsProvider` / `useModels` | `app/lib/models-context.tsx` | The client's single model list, from `/api/models`. Whatever changes what is offered calls `refresh()`: the Gateway page after a toggle, a key save or removal, or a sync, and the first-run wizard after saving keys. Everything else only reads, PulseChat included. |
44
54
  | `WizardPageClient` + step components | `app/wizard/PageClient.tsx` | Multi-step first-run wizard plus its frame shell, with mobile-first CSS. |
45
55
  | `useWizard` | `app/wizard/useWizard.ts` | Shared hook: wizard state, step transitions, provider save, restart. Receives server-side initial state to avoid a loading flash. |
46
56
  | Wizard icons | `app/wizard/icons.tsx` | Shared SVG icons (flyweight pattern): `ArrowRightIcon`, `CheckIcon`, `CheckCircleIcon`, `DockerIcon`, `GatewayIcon`, `AgentIcon`, `TemplateIcon`, `LinkIcon`, `ConfigIcon`, `InformationIcon`. |
@@ -62,6 +72,25 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
62
72
  6c. **An optimistic update moves every field the render derives from, together.** A row that decides its state by reading two fields against each other flips to a third, wrong state if the update touches only one of them — and holds it until the slow refresh lands. Update the whole set the derivation reads, or none of it.
63
73
  7. **New UI component** — add it to `app/components/ui/`, export from `index.ts`, document it here. If it's specific to one page, keep it page-local unless another page needs it.
64
74
 
75
+ 8. **`×` means remove, `✕` means dismiss.** They look alike and are not the same
76
+ action. Removing an item from a list uses `RemoveButton`; dismissing a dialog
77
+ is the `Modal` component's own close control. Do not build a third variant of
78
+ either, and do not reuse one for the other.
79
+
80
+ *Outstanding:* four dialogs predate the shared `Modal` and roll their own close
81
+ control — the edit dialog and the `openclaw.json` dialog in
82
+ `app/agents/PageClient.tsx` (both a `ghost` Button with `✕`), the drawer in
83
+ `app/components/SessionDrawer.tsx` (a bare `<button>`), and
84
+ `app/components/ModelPickerModal.tsx` (`model-picker__close`, which compounds
85
+ the problem by using `×`, the *remove* glyph, to dismiss, and omits
86
+ `type="button"`). The fix is to move them onto `Modal`, not to extract a
87
+ `CloseButton`: they also reimplement Escape-to-close and overlay behaviour.
88
+ `ModelPickerModal` has no importers at all, so deleting it is the cheaper fix
89
+ there.
90
+
91
+ Separately, `app/agents/PageClient.tsx` has a raw `×` delete-backup button with
92
+ no accessible name that should be `RemoveButton`.
93
+
65
94
  ## GoF pattern mapping
66
95
 
67
96
  Already used:
package/docs/REV4A.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Rev4a — VPS Dashboard
2
2
 
3
- > **Last updated:** 2026-09-05
3
+ > **Last updated:** 2026-09-15
4
4
 
5
5
  A Next.js 16 dashboard for monitoring and managing the OpenClaw ecosystem.
6
6
 
@@ -68,6 +68,7 @@ Rev4a reads the following environment variables. Set them in `/config` (UI) or d
68
68
  | `REV4A_ROOT` | No | Installation root, used by `rev4a update` to detect a git checkout |
69
69
  | `REV4A_TIMEZONE` | No | IANA timezone used for scheduling and timestamps |
70
70
  | `REV4A_FORCE_INSECURE_COOKIE` | No | Allow a non-Secure session cookie — plain-HTTP deployments only |
71
+ | `REV4A_AGENT_IMAGE_REGISTRY` | No | Registry repository agent images are pulled from (default: `ghcr.io/flame0510/rev4a/openclaw-agent-base`); a `localhost` registry is reached over plain HTTP |
71
72
 
72
73
  See [Alerts](#alerts-telegram) below for the Telegram alert variables.
73
74
 
@@ -76,9 +77,9 @@ See [Alerts](#alerts-telegram) below for the Telegram alert variables.
76
77
  Every agent is created with:
77
78
 
78
79
  - A host port mapping (`-p <port>:3000`) for direct access
79
- - URL format: `http://<host-public-ip>:<port>#token=...`
80
+ - Control UI at `http://<host>:<port>`, opened from the Agents page through `GET /api/agents/[id]/open-control-ui`
80
81
  - The provider gateway at `http://host.docker.internal:3740/api/provider/v1`
81
- - `allowedOrigins: ['*']` for browser Control UI access
82
+ - `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback: true`, so the Control UI opens on whatever host the agent is reached on, and pages from other origins are refused
82
83
 
83
84
  The port is auto-assigned starting from 3000 (or user-specified).
84
85
 
@@ -120,7 +121,7 @@ When environment variables are saved via the Config page (Save & Restart):
120
121
  | `/plugins` | Plugin manager |
121
122
  | `/skills` | Skill registry — browse/edit all skills (shared, per-agent workspace, bundled). Promote agent skills to shared with one click. |
122
123
  | `/tools` | Tool configuration |
123
- | `/gateway` | LLM provider sync + agent model configuration — see [GATEWAY.md](dev/GATEWAY.md) |
124
+ | `/gateway` | LLM provider keys, model catalogue and sync — see [GATEWAY.md](dev/GATEWAY.md) |
124
125
  | `/config` | Environment management and server restart |
125
126
  | `/login` | Authentication page |
126
127
 
@@ -188,6 +189,18 @@ sudo systemctl status rev4a.service
188
189
  sudo journalctl -u rev4a -f
189
190
  ```
190
191
 
192
+ ### Start-up and Docker
193
+
194
+ `rev4a serve` waits for the Docker daemon before its Docker-dependent steps, up to
195
+ `REV4A_DOCKER_WAIT_SECONDS` (default 90, capped at 600; `0` checks once without
196
+ waiting; a negative or non-numeric value means 90). It asks the daemon itself with
197
+ `docker info`, so the wait works the same on any Docker install. The variable is
198
+ read from the process environment, not from the `.env` file.
199
+
200
+ If the daemon never answers, `serve` starts anyway, skips the network and image
201
+ checks, and says so in the log. The startup sync then reaches no agent, and logs
202
+ that too; run Sync All Agents once Docker is up.
203
+
191
204
  ### Systemd environment override
192
205
 
193
206
  File: `/etc/systemd/system/rev4a-next.service` (EnvironmentFile)
@@ -290,9 +303,10 @@ Lists all agents (Docker containers with `AGENT_ID` label), with:
290
303
  - Name, image, template, status, ports, IP
291
304
  - TG channel status chip (green = connected, hidden if not configured)
292
305
  - **Shared gateway token** — text input with Save & Sync. Changing the token saves it to `data/agents-token.json` and pushes it to all running containers.
293
- - **Control UI link** — direct Traefik URL with `#token=<token>` hash, reads the token from `agents-token.json` (source of truth), not from the container's local config.
306
+ - **Control UI link** — the **Open** buttons go through `GET /api/agents/[id]/open-control-ui`, which redirects to the agent's published port on the host the dashboard was reached on: a one-time link on OpenClaw 9.x, the plain `#token=` link otherwise, with the token read inside the container.
294
307
  - **Agent creation wizard** — multi-step form at `/agents/create`.
295
- - **Channel Manager** — each agent detail panel has a Channels section with Telegram configuration and pairing management.
308
+ - **Channel Manager** — each agent detail panel has a Channels section with Telegram configuration and pairing management. Pending requests (a sender's `/start` creates one) show an **Approve** button; approved senders show **Revoke** with a confirmation. Approved senders are OpenClaw's pairing store, not the agent config: on 2026.9.x the SQLite store, read and revoked through OpenClaw's own store functions (see [ARCHITECTURE.md](ARCHITECTURE.md)).
309
+ - **Browser access** — the detail panel section right after Channels. From OpenClaw 9.x each new browser must be approved once before the Control UI connects: the section lists requests waiting for approval (Approve / Reject) and approved browsers (Rename / Revoke), and the agent card shows a "browser waiting" badge. The **Open** buttons first ask the agent for a one-time link that pairs the browser with no approval, and open the plain token link when none can be issued (every 2026.7.x agent). **Invite link** (9.x agents only) gives a link to send to someone else: their browser passes gateway auth and waits in the approval list until approved. The link carries the gateway token every agent shares; changing the agents token invalidates every link sent. All of it runs the OpenClaw CLI inside the container asynchronously.
296
310
 
297
311
  ### Agent Creation Wizard (`/agents/create`)
298
312
 
@@ -306,19 +320,20 @@ Available templates:
306
320
 
307
321
  **Step 2 — Config:** Enter agent name, optional port, select a primary model from the available providers (pre-filtered by configured provider keys). The first model is pre-selected.
308
322
 
309
- **Step 3 — Deploy:** `POST /api/agents/create` creates the Docker container and returns the Traefik URL with the shared gateway token.
323
+ **Step 3 — Deploy:** `POST /api/agents/create` creates the Docker container and returns its port and the shared gateway token; the wizard links to `http://<host>:<port>#token=<token>`.
310
324
 
311
325
  **Post-creation pipeline:**
312
- 1. The container boots with the **openclaw-agent-base:latest** image — see [Agent Templates](#agent-templates) below.
326
+ 1. The container boots with the newest supported OpenClaw version downloaded here, `openclaw-agent-base:<version>`; the create answers `409` when none is — see [Agent Templates](#agent-templates) below.
313
327
  2. OpenClaw gateway starts automatically with `--allow-unconfigured`, generating its own default config.
314
- 3. The route waits for the gateway to be fully ready (health check poll, up to 60 s).
328
+ 3. The route waits for the gateway to finish starting with `waitForGatewayReady()`, up
329
+ to 60 s. On OpenClaw 9.x it waits for `/startupz` to report `started`. The
330
+ 2026.7.1-2 image has no `/startupz`, so there it waits for `/health`, which only
331
+ shows the server is listening.
315
332
  4. Once ready, the route writes:
316
- - `gateway.controlUi.allowedOrigins` — Rev4a dashboard URL + Traefik hostname (required for browser Control UI access)
333
+ - `gateway.controlUi` with the browser-origin policy (`withControlUiPolicy()` in `lib/agent-setup.ts`): the Host-header fallback on, so the Control UI is accepted from the host the browser connected to — a public IP is refused without it — while pages from other origins are refused; an `allowedOrigins: ["*"]` list and the retired `dangerouslyDisableDeviceAuth` are dropped, any other allowlist is kept. Recreate re-applies the policy. Each new browser still needs a one-time device approval
317
334
  - `agents.defaults.model.primary` + fallbacks — the primary model selected in the wizard
318
- 5. `syncAgent(name)` writes `models.providers.rev4a` into the container.
319
- 6. The agent's control UI is immediately accessible at `https://<name>.<your-domain>.com#token=<gateway-token>`.
320
-
321
- **Key difference from the old pipeline:** the entrypoint no longer generates `openclaw.json`. The bootstrap config (including `controlUi.allowedOrigins`) is written by the create route after the gateway has already started. This eliminates the race condition where the entrypoint would overwrite synced provider config on boot.
335
+ 5. The route builds `models.providers.rev4a` with `buildRev4aProviderConfig()` and writes it with `openclaw config patch --stdin`.
336
+ 6. The agent's Control UI is reachable at `http://<host>:<port>#token=<gateway-token>`. For HTTPS or a domain, put your own reverse proxy in front of the published port: Rev4a sets no reverse-proxy labels on agent containers.
322
337
 
323
338
  ---
324
339
 
@@ -328,8 +343,9 @@ Each agent is created with a **persistent Docker volume** `agent-<name>-data`
328
343
  mounted at `/root`. This cleanly separates two planes:
329
344
 
330
345
  - **Software → image.** OpenClaw, `gh`, `vercel`, `supabase`, `trello`, Node — all
331
- live in `openclaw-agent-base:latest` (outside `/root`). Updated by rebuilding the
332
- image and recreating the container.
346
+ live in the agent image `openclaw-agent-base:<version>` (outside `/root`). A recreate
347
+ keeps the agent's OpenClaw version; moving an agent to a newer version is a separate
348
+ update.
333
349
  - **Data → volume.** Config (`openclaw.json`), state DB (`openclaw.sqlite`),
334
350
  workspace files, and CLI credentials (`.config/gh`, `.local/share/com.vercel.cli`,
335
351
  `.supabase`, …) live in `/root` and survive container removal.
@@ -341,9 +357,12 @@ a re-created agent keeps the workspace the user edited.
341
357
 
342
358
  | Action | What it does |
343
359
  |---|---|
344
- | **Backup** | `tar` of `/root` (excl. npm cache) → `.tar.gz` in the `rev4a-backups` volume |
360
+ | **Backup** | `tar` of `/root` (excl. npm cache) → `.tar.gz` in the `rev4a-backups` volume, while the agent runs |
361
+ | **Cold backup** | stop → archive `/root` (excl. npm cache) as `.partial` in a helper container → read the archive back → rename → start again if it was running. Other operations on the agent answer 409 meanwhile. Survives a Rev4a restart. API only for now (`/api/agents/[id]/cold-backup`) |
362
+ | **Update** | the agent panel's OPENCLAW VERSION section, when a newer supported version is downloaded: count transcript events and cron jobs → cold pre-update backup → recreate on the new version (its data migrates one way) → wait for that version → check nothing went missing. Unused agent images are removed afterwards |
363
+ | **Rollback** | after an update: stop → restore the pre-update backup → recreate on the previous version. Everything since the backup is lost |
345
364
  | **Restore** | stop → replace `/root` with the backup (real wipe incl. dotfiles) → restart. Software/image untouched — only data goes back in time |
346
- | **Recreate** | auto-backup first (aborts if it fails) → rebuild from `openclaw-agent-base:latest` keeping the volume → picks up image/OpenClaw/CLI updates |
365
+ | **Recreate** | auto-backup first (aborts if it fails) → rebuild on the agent's own OpenClaw version (`openclaw-agent-base:<version>`, tagged or pulled when missing) keeping the volume. Never changes version |
347
366
  | **Restart** | `docker restart`, nothing else |
348
367
  | **Delete** | **destructive**: removes container **+ volume + all backups**. Cannot be undone |
349
368
 
@@ -355,19 +374,50 @@ agent means rebuilding the image + Recreate, never `update` inside the container
355
374
 
356
375
  ## Agent Templates
357
376
 
358
- ### Base Image (`openclaw-agent-base:latest`)
377
+ ### Base Image (`openclaw-agent-base:<version>`)
359
378
 
360
379
  **Dockerfile:** `agent-templates/base-image/Dockerfile`
361
380
 
381
+ **Versions.** Images are kept locally per OpenClaw version, `openclaw-agent-base:<version>`;
382
+ `:latest` is not used. The versions this release supports, newest first, and the model
383
+ `input` list for each, are in `lib/agent-versions.json`, read by the server and the
384
+ `rev4a` CLI. The Agents page banner offers the newest supported version the registry
385
+ publishes (`ghcr.io/flame0510/rev4a/openclaw-agent-base`, or `REV4A_AGENT_IMAGE_REGISTRY`);
386
+ downloading touches no agent. Create uses the newest supported version downloaded; recreate
387
+ keeps the agent's version. An agent card shows **UPDATE AVAILABLE** when a newer supported
388
+ version is downloaded.
389
+
362
390
  Built from `node:24-bookworm-slim`, includes:
363
- - OpenClaw CLI + DeepSeek provider plugin
391
+ - OpenClaw, pinned by the `OPENCLAW_VERSION` build argument and recorded as the
392
+ `org.opencontainers.image.version` label
393
+ - `gh`, `vercel`, `trello`, `supabase`
394
+ - Plugins under `/opt/openclaw-plugins`, outside the agent volume and pinned to the same
395
+ OpenClaw release: DuckDuckGo web search
364
396
  - `openssl` (for local token generation)
365
397
  - Custom entrypoint `/agent-entrypoint.sh`
366
398
 
399
+ See also [agent-templates/README.md](../agent-templates/README.md).
400
+
367
401
  **Entrypoint behavior:**
368
402
  - Resolves the auth token (file > env > local random)
369
403
  - Does NOT generate `openclaw.json` — OpenClaw creates its own default config on first boot
370
- - **Version-gated migration:** if the OpenClaw version differs from the one recorded in `/root/.openclaw/.last-version` (i.e. the image was rebuilt to a newer OpenClaw), runs `openclaw doctor --non-interactive` **before** starting the gateway. This applies only safe migrations (config normalization + on-disk state moves), skips service restarts, and aligns the persistent volume's state/config to the new binary. Best-effort — never blocks boot. Runs at most once per version change, not on every restart.
404
+ - **Version-gated migration:** if the OpenClaw version differs from the one recorded in
405
+ `/root/.openclaw/.last-version` (a newer image, or a new volume), runs
406
+ `openclaw doctor --fix --non-interactive` **before** starting the gateway. `--fix` is
407
+ required from OpenClaw 9.x: the agent database schema is migrated only with the gateway
408
+ stopped, and the gateway refuses the old schema rather than migrating it. The version is
409
+ recorded only after a successful run, so a failed migration is retried on the next boot.
410
+ A failure does not block boot.
411
+ - **Image defaults**, before the gateway starts, written only where missing:
412
+ - the image's plugin directories in `plugins.load.paths`
413
+ - `tools.web.search.provider: "duckduckgo"` when no provider is set
414
+ - `agents.defaults.heartbeat.every: "0m"` when unset: every heartbeat tick is a
415
+ model-calling turn, so the recurring cadence is opt-in
416
+
417
+ An explicit value, such as a heartbeat turned on for one agent, is never overwritten.
418
+ The config is read with one node process and written through `openclaw config patch`,
419
+ which validates; if `openclaw.json` cannot be read as JSON, the defaults are skipped and
420
+ the log says so.
371
421
  - Starts OpenClaw gateway with `--allow-unconfigured`: `openclaw gateway --bind lan --port 3000 --auth token --token "<token>" --allow-unconfigured`
372
422
  - Everything else (controlUi, model refs, providers) is handled by the create route and the Gateway sync module
373
423
 
@@ -385,23 +435,33 @@ repo. The copy is skipped when the workspace is already populated (see
385
435
 
386
436
  ## Gateway Page (`/gateway`)
387
437
 
388
- Two panels:
438
+ One job: provider configuration. API keys, which catalogue models this deployment
439
+ offers, and pushing that catalogue to every agent.
440
+
441
+ - Reads the model catalogue from `models.config.json`, merged with user toggles in
442
+ `<data dir>/model-overrides.json`
443
+ - Scans providers with keys in `<data dir>/provider-keys.json`
444
+ - `PUT /api/gateway/provider` runs `syncAllAgents()`, which writes
445
+ `models.providers.rev4a` into every agent container
446
+ - **Does NOT touch** `agents.defaults.model` or `agents.list[].model` — those are
447
+ per-agent settings
448
+
449
+ ### Per-agent model, elsewhere
389
450
 
390
- ### Provider Sync
391
- - Reads active models from `models.config.json` (file-based model catalogue)
392
- - Scans providers with keys in `data/provider-keys.json`
393
- - `PUT /api/gateway/provider` runs `syncAllAgents()` which writes `models.providers.rev4a` to every agent container
394
- - **Does NOT touch** `agents.defaults.model` or `agents.list[].model` — model references are managed per-container from the Agents Tab
451
+ Choosing which model one agent runs is not on this page. It lives in the Model
452
+ section of that agent's detail panel on the Agents page
453
+ (`app/agents/ModelSection.tsx`), which calls `PUT /api/gateway/agent` to write the
454
+ model ref into that container's `openclaw.json`. No gateway restart: OpenClaw
455
+ watches the file and hot-applies the change.
395
456
 
396
- ### Agent Model Config
397
- - Select a container and set its primary model
398
- - `PUT /api/gateway/agent` writes the model ref (`primary`, `fallbacks`) directly into the container's `openclaw.json`
399
- - Does NOT restart the gateway — writes are live via file write
457
+ The model is a property of the agent, not of the gateway. See
458
+ [GATEWAY.md](dev/GATEWAY.md) for the write semantics: omitting `fallbacks` keeps the
459
+ agent's current list, `[]` clears it.
400
460
 
401
461
  ### Model Catalogue (`models.config.json`)
402
462
  File-based, tracked in git. Each entry:
403
463
  ```json
404
- { "id": "deepseek/deepseek-v4-flash", "name": "DeepSeek V4 Flash", "provider": "deepseek", "enabled": true }
464
+ { "id": "deepseek/deepseek-flash", "name": "DeepSeek Flash", "provider": "deepseek", "enabled": true }
405
465
  ```
406
466
  Models with `enabled: false` are ignored.
407
467
  Only models whose provider has a key in `data/provider-keys.json` are synced.
@@ -414,7 +474,7 @@ Only models whose provider has a key in `data/provider-keys.json` are synced.
414
474
 
415
475
  | Property | Value |
416
476
  |---|---|
417
- | Image | `openclaw-agent-base:latest` (Node 24-bookworm-slim + OpenClaw) |
477
+ | Image | `openclaw-agent-base:<version>` (Node 24-bookworm-slim + OpenClaw) |
418
478
  | Port | `0.0.0.0:3731 → 3000` |
419
479
  | IP | `172.19.0.3` |
420
480
  | AGENT_ID | `atlas` |