@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.
- package/README.md +3 -1
- package/agent-templates/README.md +42 -22
- package/agent-templates/base-image/Dockerfile +42 -33
- package/agent-templates/base-image/entrypoint.sh +67 -12
- package/app/agents/BrowserAccessSection.tsx +510 -0
- package/app/agents/ChannelManager.tsx +19 -11
- package/app/agents/ImageDownloadBanner.tsx +53 -19
- package/app/agents/ModelSection.tsx +316 -0
- package/app/agents/PageClient.tsx +708 -167
- package/app/agents/UpdateSection.tsx +300 -0
- package/app/agents/create/PageClient.tsx +11 -49
- package/app/agents/create/page.tsx +8 -21
- package/app/api/agents/[id]/backup/route.ts +26 -69
- package/app/api/agents/[id]/channels/pairing/route.ts +3 -3
- package/app/api/agents/[id]/channels/telegram/route.ts +2 -2
- package/app/api/agents/[id]/cold-backup/route.ts +56 -0
- package/app/api/agents/[id]/devices/route.ts +126 -0
- package/app/api/agents/[id]/invite-link/route.ts +53 -0
- package/app/api/agents/[id]/lifecycle/route.ts +3 -0
- package/app/api/agents/[id]/model/route.ts +113 -0
- package/app/api/agents/[id]/open-control-ui/route.ts +58 -0
- package/app/api/agents/[id]/recreate/route.ts +33 -187
- package/app/api/agents/[id]/restart/route.ts +5 -0
- package/app/api/agents/[id]/restore/route.ts +40 -70
- package/app/api/agents/[id]/route.ts +38 -169
- package/app/api/agents/[id]/update/rollback/route.ts +30 -0
- package/app/api/agents/[id]/update/route.ts +50 -0
- package/app/api/agents/activity-summary/route.ts +67 -0
- package/app/api/agents/create/route.ts +91 -145
- package/app/api/agents/devices-summary/route.ts +37 -0
- package/app/api/agents/download-image/route.ts +16 -9
- package/app/api/agents/image-status/route.ts +31 -111
- package/app/api/agents/models-summary/route.ts +163 -0
- package/app/api/agents/route.ts +25 -49
- package/app/api/agents/token/route.ts +33 -10
- package/app/api/assistant/route.ts +37 -16
- package/app/api/gateway/agent/route.ts +37 -6
- package/app/api/gateway/provider/balance/route.ts +5 -2
- package/app/api/gateway/provider/keys.ts +13 -1
- package/app/api/gateway/provider/route.ts +43 -12
- package/app/api/gateway/sync.ts +335 -76
- package/app/api/models/route.ts +28 -34
- package/app/api/provider/auth.ts +65 -0
- package/app/api/provider/upstream.ts +9 -2
- package/app/api/provider/v1/chat/completions/route.ts +22 -16
- package/app/api/provider/v1/models/route.ts +26 -133
- package/app/api/setup/agent-image/route.ts +14 -42
- package/app/components/DashboardToolbar.tsx +1 -1
- package/app/components/PulseChat.tsx +25 -39
- package/app/components/ui/RemoveButton.tsx +46 -0
- package/app/components/ui/Select.tsx +3 -2
- package/app/components/ui/index.ts +1 -0
- package/app/credentials/PageClient.tsx +2 -2
- package/app/gateway/PageClient.tsx +253 -674
- package/app/globals.css +8 -0
- package/app/lib/models-context.tsx +43 -7
- package/app/wizard/useWizard.ts +6 -1
- package/bin/rev4a.js +116 -50
- package/daemon.js +6 -6
- package/docs/ARCHITECTURE.md +110 -12
- package/docs/FRONTEND-ARCHITECTURE.md +31 -2
- package/docs/REV4A.md +93 -33
- package/docs/dev/API-REFERENCE.md +723 -178
- package/docs/dev/DATABASE.md +96 -0
- package/docs/dev/GATEWAY.md +250 -93
- package/docs/dev/PROVIDERS.md +26 -13
- package/docs/rag/DATA-FRESHNESS.md +59 -28
- package/docs/rag/GLOSSARY.md +27 -16
- package/docs/rag/REV4A-OVERVIEW.md +37 -25
- package/docs/rag/WHAT-I-CAN-ANSWER.md +10 -8
- package/instrumentation.ts +52 -1
- package/lib/agent-busy.ts +21 -0
- package/lib/agent-devices.ts +361 -0
- package/lib/agent-edit-state.ts +108 -0
- package/lib/agent-edit.ts +157 -0
- package/lib/agent-images.ts +375 -0
- package/lib/agent-ports-server.ts +27 -0
- package/lib/agent-ports.ts +68 -0
- package/lib/agent-readiness.ts +110 -0
- package/lib/agent-recreate-state.ts +108 -0
- package/lib/agent-recreate.ts +305 -0
- package/lib/agent-restore-state.ts +107 -0
- package/lib/agent-restore.ts +135 -0
- package/lib/agent-setup.ts +66 -17
- package/lib/agent-update-state.ts +122 -0
- package/lib/agent-update.ts +448 -0
- package/lib/agent-versions.json +14 -0
- package/lib/agent-versions.ts +80 -0
- package/lib/buildAgentImage.ts +88 -290
- package/lib/channelManager.ts +153 -64
- package/lib/cold-backup.ts +354 -0
- package/lib/container-file.ts +27 -0
- package/lib/credentials/delivery.ts +3 -3
- package/lib/db-bootstrap.mjs +76 -0
- package/lib/docker-utils.ts +3 -3
- package/lib/model-catalogue.ts +140 -27
- package/lib/provider-balance.ts +33 -12
- package/lib/rev4a-paths.ts +0 -21
- package/model-pricing.json +118 -110
- package/models.config.json +27 -12
- package/package.json +1 -1
- package/app/api/gateway/route.ts +0 -191
package/app/globals.css
CHANGED
|
@@ -1296,6 +1296,14 @@ html, body { height: 100%; height: 100dvh; background: var(--bg); color: var(--t
|
|
|
1296
1296
|
background: rgba(var(--red-rgb,239,68,68),0.1);
|
|
1297
1297
|
color: var(--red);
|
|
1298
1298
|
}
|
|
1299
|
+
/* Saved, but not fully applied — e.g. a change stored while an agent sync failed. */
|
|
1300
|
+
.msg-warning {
|
|
1301
|
+
padding: 8px 12px;
|
|
1302
|
+
border-radius: 0;
|
|
1303
|
+
font-size: 12px;
|
|
1304
|
+
background: rgba(245,158,11,0.1);
|
|
1305
|
+
color: var(--yellow);
|
|
1306
|
+
}
|
|
1299
1307
|
|
|
1300
1308
|
/* --- Mobile Bottom Navigation --- */
|
|
1301
1309
|
.mobile-bottom-nav {
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The model list, fetched once and shared by every client that needs it.
|
|
5
|
+
*
|
|
6
|
+
* There used to be two independent copies: this provider, and a private fetch
|
|
7
|
+
* inside PulseChat. Both ran on mount with an empty dependency list, and neither
|
|
8
|
+
* component ever unmounts — so unchecking a model on the Gateway page left the
|
|
9
|
+
* chat panel offering it for the rest of the session, and only a full page reload
|
|
10
|
+
* cleared it.
|
|
11
|
+
*
|
|
12
|
+
* The fix is not to refetch more often. It is to refetch when the answer
|
|
13
|
+
* changes: `refresh()` is called by whoever changes it — the Gateway page after a
|
|
14
|
+
* model toggle, a key save or removal, and a sync; the first-run wizard after
|
|
15
|
+
* saving keys. Everything else just reads.
|
|
16
|
+
*/
|
|
17
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
4
18
|
|
|
5
19
|
interface ProviderEntry {
|
|
6
20
|
provider: string;
|
|
@@ -11,26 +25,48 @@ interface ProviderEntry {
|
|
|
11
25
|
interface ModelsContextValue {
|
|
12
26
|
providers: ProviderEntry[];
|
|
13
27
|
loaded: boolean;
|
|
28
|
+
/** Re-read the catalogue. Call after changing which models are enabled. */
|
|
29
|
+
refresh: () => void;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
|
-
const ModelsContext = createContext<ModelsContextValue>({
|
|
32
|
+
const ModelsContext = createContext<ModelsContextValue>({
|
|
33
|
+
providers: [],
|
|
34
|
+
loaded: false,
|
|
35
|
+
refresh: () => {},
|
|
36
|
+
});
|
|
17
37
|
|
|
18
38
|
export function ModelsProvider({ children }: { children: React.ReactNode }) {
|
|
19
39
|
const [providers, setProviders] = useState<ProviderEntry[]>([]);
|
|
20
40
|
const [loaded, setLoaded] = useState(false);
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
// Refreshes can overlap — two quick toggles — and responses can arrive out of
|
|
43
|
+
// order. Only the most recent request may write.
|
|
44
|
+
const seq = useRef(0);
|
|
45
|
+
|
|
46
|
+
const refresh = useCallback(() => {
|
|
47
|
+
const mine = ++seq.current;
|
|
23
48
|
fetch('/api/models')
|
|
24
|
-
.then((r) =>
|
|
49
|
+
.then((r) => {
|
|
50
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
51
|
+
return r.json();
|
|
52
|
+
})
|
|
25
53
|
.then((data) => {
|
|
26
|
-
if (
|
|
54
|
+
if (mine !== seq.current) return;
|
|
55
|
+
// An empty array is a real answer — every model disabled, or no provider
|
|
56
|
+
// key — so it must replace the list rather than be discarded as a failure.
|
|
57
|
+
if (Array.isArray(data)) setProviders(data);
|
|
27
58
|
setLoaded(true);
|
|
28
59
|
})
|
|
29
|
-
.catch(() =>
|
|
60
|
+
.catch(() => {
|
|
61
|
+
// Keep the previous list: a failed refresh is not an empty catalogue.
|
|
62
|
+
if (mine === seq.current) setLoaded(true);
|
|
63
|
+
});
|
|
30
64
|
}, []);
|
|
31
65
|
|
|
66
|
+
useEffect(() => { refresh(); }, [refresh]);
|
|
67
|
+
|
|
32
68
|
return (
|
|
33
|
-
<ModelsContext.Provider value={{ providers, loaded }}>
|
|
69
|
+
<ModelsContext.Provider value={{ providers, loaded, refresh }}>
|
|
34
70
|
{children}
|
|
35
71
|
</ModelsContext.Provider>
|
|
36
72
|
);
|
package/app/wizard/useWizard.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import { useModels } from '../lib/models-context';
|
|
4
5
|
import {
|
|
5
6
|
isWizardComplete,
|
|
6
7
|
markWizardComplete,
|
|
@@ -45,6 +46,9 @@ export function useWizard() {
|
|
|
45
46
|
// Step tracking is in-memory only; the cookie is the source of truth
|
|
46
47
|
}, []);
|
|
47
48
|
|
|
49
|
+
// Saving a key changes which models are offered; the shared list must follow.
|
|
50
|
+
const { refresh: refreshOfferedModels } = useModels();
|
|
51
|
+
|
|
48
52
|
const saveProviders = useCallback(async () => {
|
|
49
53
|
const entries = Object.entries(providerKeys).filter(([, v]) => v.trim());
|
|
50
54
|
if (entries.length === 0) {
|
|
@@ -60,10 +64,11 @@ export function useWizard() {
|
|
|
60
64
|
body: JSON.stringify({ provider, apiKey: apiKey.trim() }),
|
|
61
65
|
});
|
|
62
66
|
}
|
|
67
|
+
refreshOfferedModels();
|
|
63
68
|
markStep('providers');
|
|
64
69
|
} catch { /* API call failed — do not mark step as complete */ }
|
|
65
70
|
setSaving(false);
|
|
66
|
-
}, [providerKeys, markStep]);
|
|
71
|
+
}, [providerKeys, markStep, refreshOfferedModels]);
|
|
67
72
|
|
|
68
73
|
const goNext = useCallback(async () => {
|
|
69
74
|
if (step === 'welcome') { markStep('welcome'); setStep('providers'); }
|
package/bin/rev4a.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* rev4a serve:
|
|
11
11
|
* 1. Ensures .env exists (auto-generates on first run)
|
|
12
12
|
* 2. Ensures .next build exists (builds if missing)
|
|
13
|
-
* 3. Ensures Docker is installed (installs if missing + root)
|
|
13
|
+
* 3. Ensures Docker is installed (installs if missing + root) and waits for its daemon
|
|
14
14
|
* 4. Ensures agent base image is built (builds if missing)
|
|
15
15
|
* 5. Starts Next.js + daemon + terminal WS as a single process group
|
|
16
16
|
*
|
|
@@ -41,9 +41,20 @@ const DATA_DIR = join(CONFIG_DIR, 'data');
|
|
|
41
41
|
const ENV_SOURCE = join(CONFIG_DIR, '.env');
|
|
42
42
|
const ENV_LINK = join(ROOT, '.env');
|
|
43
43
|
const DOCKERFILE = join(ROOT, 'agent-templates', 'base-image', 'Dockerfile');
|
|
44
|
-
|
|
45
|
-
const
|
|
44
|
+
// Supported OpenClaw versions and image repositories, shared with the server.
|
|
45
|
+
const AGENT_VERSIONS = require(join(ROOT, 'lib', 'agent-versions.json'));
|
|
46
46
|
const DOCKER_NETWORK = 'rev4a-network';
|
|
47
|
+
// How long `serve` waits for the Docker daemon before starting without it. Like
|
|
48
|
+
// REV4A_DATA_DIR it is read from the real process environment (a systemd
|
|
49
|
+
// Environment= line, a shell export), because the .env file is loaded later.
|
|
50
|
+
const DOCKER_WAIT_SECONDS = (() => {
|
|
51
|
+
const raw = (process.env.REV4A_DOCKER_WAIT_SECONDS || '').trim();
|
|
52
|
+
if (!raw) return 90;
|
|
53
|
+
const n = Number(raw);
|
|
54
|
+
// 0 means a single check. Non-numeric or infinite values fall back to the
|
|
55
|
+
// default, and the ceiling keeps a typo from holding start-up for hours.
|
|
56
|
+
return Number.isFinite(n) && n >= 0 ? Math.min(n, 600) : 90;
|
|
57
|
+
})();
|
|
47
58
|
|
|
48
59
|
// Fallback used only when package.json cannot be read — the published npm
|
|
49
60
|
// package name is the single source of truth, so never hardcode it elsewhere.
|
|
@@ -97,25 +108,70 @@ function whichDistro() {
|
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
// ── Docker auto-install
|
|
111
|
+
// ── Docker auto-install and daemon wait ─────────────────────────────────────
|
|
101
112
|
|
|
113
|
+
/** Block this thread for `ms` without spawning a process (`sleep` is not on every OS). */
|
|
114
|
+
function sleepSync(ms) {
|
|
115
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Wait until the Docker daemon answers, up to DOCKER_WAIT_SECONDS.
|
|
120
|
+
*
|
|
121
|
+
* Finding the `docker` command only proves the CLI is installed. At boot the
|
|
122
|
+
* daemon can still be starting — on the production host systemd started this
|
|
123
|
+
* service a second before Docker. Start-up still worked, by accident: the first
|
|
124
|
+
* `docker` call blocked on docker.socket until the daemon was up. With a daemon
|
|
125
|
+
* slower than a later call's 10 s timeout, the image check would have concluded
|
|
126
|
+
* the image was missing and tried to pull and rebuild it. Asking the daemon
|
|
127
|
+
* directly, with a short per-attempt timeout, makes the wait explicit, bounded
|
|
128
|
+
* and logged, whatever kind of Docker install this is.
|
|
129
|
+
*/
|
|
130
|
+
function waitForDockerDaemon() {
|
|
131
|
+
const deadline = Date.now() + DOCKER_WAIT_SECONDS * 1000;
|
|
132
|
+
let lastError = '';
|
|
133
|
+
let announced = false;
|
|
134
|
+
for (;;) {
|
|
135
|
+
try {
|
|
136
|
+
const version = execSync('docker info --format "{{.ServerVersion}}"', { stdio: 'pipe', timeout: 10_000 })
|
|
137
|
+
.toString().trim();
|
|
138
|
+
if (version) {
|
|
139
|
+
log('DOCKER', `Docker daemon ready (server ${version})`);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
lastError = ((err.stderr && err.stderr.toString()) || err.message || '').trim().split('\n')[0];
|
|
144
|
+
}
|
|
145
|
+
if (Date.now() >= deadline) break;
|
|
146
|
+
if (!announced) {
|
|
147
|
+
log('DOCKER', `Waiting for the Docker daemon (up to ${DOCKER_WAIT_SECONDS}s)…`);
|
|
148
|
+
announced = true;
|
|
149
|
+
}
|
|
150
|
+
sleepSync(2000);
|
|
151
|
+
}
|
|
152
|
+
log('DOCKER', `⚠ Docker daemon not reachable after ${DOCKER_WAIT_SECONDS}s: ${lastError || 'no answer'}`);
|
|
153
|
+
log('DOCKER', ' Starting without it. Agents are unavailable and the startup sync reaches none; run Sync All Agents once Docker is up.');
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** True when the Docker daemon is reachable and the Docker-dependent steps can run. */
|
|
102
158
|
function ensureDocker() {
|
|
103
159
|
if (hasCmd('docker')) {
|
|
104
|
-
log('DOCKER', 'Docker
|
|
105
|
-
return;
|
|
160
|
+
log('DOCKER', 'Docker CLI found');
|
|
161
|
+
return waitForDockerDaemon();
|
|
106
162
|
}
|
|
107
163
|
|
|
108
164
|
if (!isLinux()) {
|
|
109
165
|
log('DOCKER', '⚠ Docker not found. Rev4a needs Docker to create agent containers.');
|
|
110
166
|
log('DOCKER', ' Install Docker: https://docs.docker.com/get-docker/');
|
|
111
167
|
log('DOCKER', ' Then run: rev4a serve');
|
|
112
|
-
return;
|
|
168
|
+
return false;
|
|
113
169
|
}
|
|
114
170
|
|
|
115
171
|
if (!isRoot()) {
|
|
116
172
|
log('DOCKER', '⚠ Docker not found. Run Rev4a as root to auto-install:');
|
|
117
173
|
log('DOCKER', ' sudo rev4a serve');
|
|
118
|
-
return;
|
|
174
|
+
return false;
|
|
119
175
|
}
|
|
120
176
|
|
|
121
177
|
const distro = whichDistro();
|
|
@@ -134,10 +190,12 @@ function ensureDocker() {
|
|
|
134
190
|
);
|
|
135
191
|
|
|
136
192
|
log('DOCKER', 'Docker installed successfully');
|
|
193
|
+
return waitForDockerDaemon();
|
|
137
194
|
} catch (err) {
|
|
138
195
|
log('DOCKER', '⚠ Docker auto-install failed. Install manually:');
|
|
139
196
|
log('DOCKER', ' https://docs.docker.com/engine/install/');
|
|
140
197
|
log('DOCKER', ' Then run: rev4a serve');
|
|
198
|
+
return false;
|
|
141
199
|
}
|
|
142
200
|
}
|
|
143
201
|
|
|
@@ -146,55 +204,57 @@ function ensureDocker() {
|
|
|
146
204
|
function ensureAgentImage() {
|
|
147
205
|
if (!hasCmd('docker')) return; // Docker not available — skip, warn later
|
|
148
206
|
|
|
149
|
-
//
|
|
207
|
+
// The newest OpenClaw version this release supports, kept under its version tag.
|
|
208
|
+
// `:latest` is never used: an agent's version decides its data schema.
|
|
209
|
+
const version = AGENT_VERSIONS.supported[0].version;
|
|
210
|
+
const localRef = `${AGENT_VERSIONS.localRepository}:${version}`;
|
|
211
|
+
const registry = (process.env.REV4A_AGENT_IMAGE_REGISTRY || '').trim() || AGENT_VERSIONS.registryRepository;
|
|
212
|
+
const remoteRef = `${registry}:${version}`;
|
|
213
|
+
|
|
150
214
|
try {
|
|
151
|
-
const r = execSync(
|
|
152
|
-
`docker image inspect ${AGENT_IMAGE} --format '{{.Id}}'`,
|
|
153
|
-
{ stdio: 'pipe', timeout: 10_000 },
|
|
154
|
-
);
|
|
215
|
+
const r = execSync(`docker image inspect ${localRef} --format '{{.Id}}'`, { stdio: 'pipe', timeout: 10_000 });
|
|
155
216
|
if (r.toString().trim()) {
|
|
156
|
-
log('IMAGE', `Agent base image ${
|
|
217
|
+
log('IMAGE', `Agent base image ${localRef} already exists`);
|
|
157
218
|
return;
|
|
158
219
|
}
|
|
159
220
|
} catch {
|
|
160
|
-
//
|
|
221
|
+
// Not downloaded yet
|
|
161
222
|
}
|
|
162
223
|
|
|
163
|
-
log('IMAGE', `Pulling agent base image
|
|
164
|
-
|
|
224
|
+
log('IMAGE', `Pulling agent base image ${remoteRef}…`);
|
|
165
225
|
try {
|
|
166
|
-
execSync(`docker pull ${
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
timeout: 10_000,
|
|
174
|
-
});
|
|
175
|
-
log('IMAGE', `Agent base image pulled and tagged as ${AGENT_IMAGE}`);
|
|
226
|
+
execSync(`docker pull ${remoteRef}`, { stdio: 'inherit', timeout: 1_800_000 });
|
|
227
|
+
execSync(`docker tag ${remoteRef} ${localRef}`, { stdio: 'inherit', timeout: 10_000 });
|
|
228
|
+
try {
|
|
229
|
+
execSync(`docker rmi ${remoteRef}`, { stdio: 'pipe', timeout: 30_000 }); // untag; the layers stay
|
|
230
|
+
} catch { /* keeping the registry reference is harmless */ }
|
|
231
|
+
log('IMAGE', `Agent base image pulled and tagged as ${localRef}`);
|
|
232
|
+
return;
|
|
176
233
|
} catch {
|
|
177
|
-
log('IMAGE',
|
|
178
|
-
|
|
179
|
-
if (!existsSync(DOCKERFILE)) {
|
|
180
|
-
log('IMAGE', `⚠ Agent base image Dockerfile not found at ${DOCKERFILE}`);
|
|
181
|
-
log('IMAGE', ' Agent creation will be unavailable until the image is available.');
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
234
|
+
log('IMAGE', '⚠ Pull from registry failed');
|
|
235
|
+
}
|
|
184
236
|
|
|
185
|
-
|
|
186
|
-
|
|
237
|
+
// A local build is only the same image when this Dockerfile builds that version.
|
|
238
|
+
const dockerfileVersion = existsSync(DOCKERFILE)
|
|
239
|
+
? (/^ARG OPENCLAW_VERSION=(\S+)/m.exec(readFileSync(DOCKERFILE, 'utf-8')) || [])[1] || null
|
|
240
|
+
: null;
|
|
241
|
+
if (dockerfileVersion !== version) {
|
|
242
|
+
log('IMAGE', ` The Dockerfile here builds ${dockerfileVersion || 'no version'}, not ${version}: no local build.`);
|
|
243
|
+
log('IMAGE', ' Agent creation will be unavailable until the image is downloaded from the Agents page.');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
187
246
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
247
|
+
log('IMAGE', `Building agent base image ${localRef} (this will take a few minutes)…`);
|
|
248
|
+
log('IMAGE', ' → pulling node:24-bookworm-slim + installing OpenClaw + tooling');
|
|
249
|
+
try {
|
|
250
|
+
execSync(
|
|
251
|
+
`docker build --build-arg OPENCLAW_VERSION=${version} -t ${localRef} -f "${DOCKERFILE}" "${join(DOCKERFILE, '..')}"`,
|
|
252
|
+
{ stdio: 'inherit', timeout: 3_600_000 },
|
|
253
|
+
);
|
|
254
|
+
log('IMAGE', 'Agent base image built successfully');
|
|
255
|
+
} catch {
|
|
256
|
+
log('IMAGE', '⚠ Agent base image build failed. Agent creation will be unavailable.');
|
|
257
|
+
log('IMAGE', ` Pull it manually: docker pull ${remoteRef} && docker tag ${remoteRef} ${localRef}`);
|
|
198
258
|
}
|
|
199
259
|
}
|
|
200
260
|
|
|
@@ -414,9 +474,15 @@ function checkVersion() {
|
|
|
414
474
|
function preflight() {
|
|
415
475
|
log('PREFLIGHT', 'Running pre-flight checks…');
|
|
416
476
|
ensureEnv();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
477
|
+
// Network and image need the daemon. Without it the image check fails and
|
|
478
|
+
// would start a pull and then a local build — up to fifteen minutes, before the
|
|
479
|
+
// dashboard is even up — for an image that is most likely already there.
|
|
480
|
+
if (ensureDocker()) {
|
|
481
|
+
ensureNetwork();
|
|
482
|
+
ensureAgentImage();
|
|
483
|
+
} else {
|
|
484
|
+
log('PREFLIGHT', 'Skipping network and agent image checks: Docker daemon not reachable.');
|
|
485
|
+
}
|
|
420
486
|
ensureRev4aRules();
|
|
421
487
|
|
|
422
488
|
if (!existsSync(NEXT_DIR)) {
|
|
@@ -575,7 +641,7 @@ EXAMPLES:
|
|
|
575
641
|
On first run, rev4a serve automatically:
|
|
576
642
|
• Generates .env (JWT secret + API token)
|
|
577
643
|
• Installs Docker if missing (requires root)
|
|
578
|
-
•
|
|
644
|
+
• Downloads the agent base image for the newest supported OpenClaw version
|
|
579
645
|
• Builds the Next.js app if no build is found
|
|
580
646
|
• Starts Next.js, daemon, and terminal WebSocket as a single process group
|
|
581
647
|
• Cleanly stops all services on Ctrl+C / SIGTERM
|
package/daemon.js
CHANGED
|
@@ -668,12 +668,12 @@ function pollSessions() {
|
|
|
668
668
|
// ── Timeout Detection ────────────────────────────────────────────────────
|
|
669
669
|
// Find sub-agents that were 'working' in the last snapshot but are no longer
|
|
670
670
|
// present in the current output and have been missing for more than 10 minutes.
|
|
671
|
-
//
|
|
672
|
-
const TIMEOUT_THRESHOLD_MS = 10 * 60 * 1000; // 10
|
|
671
|
+
// These are recorded as 'timeout' in the events table.
|
|
672
|
+
const TIMEOUT_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
|
|
673
673
|
for (const [session_id, snap] of knownSessions) {
|
|
674
674
|
// Only sub-agents (contain 'subagent' in the key)
|
|
675
675
|
if (!session_id.includes('subagent')) continue;
|
|
676
|
-
//
|
|
676
|
+
// Only if they were working in the last poll
|
|
677
677
|
if (snap.status !== 'working') continue;
|
|
678
678
|
// Only if not present in the current poll
|
|
679
679
|
if (currentIds.has(session_id)) continue;
|
|
@@ -694,10 +694,10 @@ function pollSessions() {
|
|
|
694
694
|
data: JSON.stringify({
|
|
695
695
|
missing_for_ms: missingFor,
|
|
696
696
|
last_status: snap.status,
|
|
697
|
-
message: `
|
|
697
|
+
message: `Subagent missing for ${Math.round(missingFor / 60000)} min without completing`,
|
|
698
698
|
}),
|
|
699
699
|
});
|
|
700
|
-
log(`[TIMEOUT] ${session_id} missing
|
|
700
|
+
log(`[TIMEOUT] ${session_id} missing for ${Math.round(missingFor / 60000)} min`);
|
|
701
701
|
|
|
702
702
|
// Notify Michele via openclaw message (only if openclaw is available)
|
|
703
703
|
try {
|
|
@@ -708,7 +708,7 @@ function pollSessions() {
|
|
|
708
708
|
'message', 'send',
|
|
709
709
|
'--account', 'ops',
|
|
710
710
|
'--target', '297086793',
|
|
711
|
-
'--text', `⚠️ Rev4a: agent timeout\n\`${session_id.slice(-36)}\`\nMissing for ${Math.round(missingFor / 60000)} min
|
|
711
|
+
'--text', `⚠️ Rev4a: agent timeout\n\`${session_id.slice(-36)}\`\nMissing for ${Math.round(missingFor / 60000)} min without completing.`,
|
|
712
712
|
], { encoding: 'utf8', timeout: 10_000, killSignal: 'SIGKILL' });
|
|
713
713
|
} catch (e) {
|
|
714
714
|
log(`[TIMEOUT] Telegram notification failed: ${e.message}`);
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Rev4a Architecture — Design & Vision
|
|
2
2
|
|
|
3
3
|
> **Status:** Active — `main` branch
|
|
4
|
-
> **Last updated:** 2026-
|
|
4
|
+
> **Last updated:** 2026-09-15
|
|
5
5
|
> **Goal:** Transform Rev4a from a monitoring dashboard into a central orchestrator for a distributed multi-container agency.
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -119,6 +119,100 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
119
119
|
- Injected into every session via the `bootstrap-extra-files` hook (glob `.rev4a/*.md`)
|
|
120
120
|
- Immutable by agents — enforced by the `:ro` mount
|
|
121
121
|
- See `lib/agent-setup.ts` for the centralized volume + config guarantee logic
|
|
122
|
+
- **Waiting for a new container** is `waitForGatewayReady()` in
|
|
123
|
+
`lib/agent-readiness.ts`, used by create, recreate and the agent route. On
|
|
124
|
+
OpenClaw 9.x it waits for `/startupz` to report `started` (503 while starting).
|
|
125
|
+
The 2026.7.1-2 image has no `/startupz` — its gateway serves the web UI with 200
|
|
126
|
+
for unknown paths — so there it falls back to `/health`, which only shows the
|
|
127
|
+
server is listening. Do not reimplement the wait in a route.
|
|
128
|
+
- **The model catalogue** is read only through `lib/model-catalogue.ts`:
|
|
129
|
+
`loadModelsConfig()`, `loadOfferedModels()` (enabled after overrides, provider has
|
|
130
|
+
a key), `isModelOffered()` (enforced by the proxy and the assistant) and
|
|
131
|
+
`catalogueStatus()`. A failed read of `models.config.json` falls back to the last
|
|
132
|
+
good copy and never deletes overrides.
|
|
133
|
+
- **Agent images and OpenClaw versions.** `lib/agent-versions.json` lists the supported
|
|
134
|
+
OpenClaw versions, newest first, with the model `input` list for each; the server reads
|
|
135
|
+
it through `lib/agent-versions.ts`, the `rev4a` CLI with `require`. The provider sync
|
|
136
|
+
takes each agent's own list (`containerVersionSync()`: image tag, else the entrypoint's
|
|
137
|
+
`.last-version` file), so a 2026.7.x agent is never sent an `input` value it would
|
|
138
|
+
reject — one unknown value discards the whole generated catalogue. The previous
|
|
139
|
+
versions stay listed while agents still run them: that is also what lets a rollback
|
|
140
|
+
pull the previous image. `lib/agent-images.ts`
|
|
141
|
+
lists local `openclaw-agent-base:<version>` images through the Docker API, reads an
|
|
142
|
+
agent's version (image label or tag, else `openclaw --version` in the container, cached
|
|
143
|
+
per image id), reads the registry tag list over HTTP (cached 10 min), pulls a version
|
|
144
|
+
and resolves the image a recreate uses. Images are never addressed as `:latest`: create
|
|
145
|
+
takes the newest supported version downloaded, recreate the agent's own.
|
|
146
|
+
- **Cold backups** are `lib/cold-backup.ts`. The agent is stopped and a detached helper
|
|
147
|
+
container, `rev4a-backup-<id>` from the agent's own image, archives its volume to
|
|
148
|
+
`rev4a-backups` as `.partial`, reads it back and renames it; the agent is started
|
|
149
|
+
again if it was running. The helper and its labels are the job, so no request waits
|
|
150
|
+
on it and a Rev4a restart does not lose it (`reconcileColdBackups()` in
|
|
151
|
+
`instrumentation.ts`). While a helper runs, the routes that change the agent answer
|
|
152
|
+
409 (`isColdBackupRunning()`), and `syncAllAgents()` skips the agent.
|
|
153
|
+
- **Agent updates** are `lib/agent-update.ts`, with their state in the `agent_upgrades`
|
|
154
|
+
table (`lib/agent-update-state.ts`). An update counts the agent's transcript events per
|
|
155
|
+
session and its cron jobs, takes a cold pre-update backup, recreates the container on
|
|
156
|
+
the newer version (`lib/agent-recreate.ts`, argv only, env values outside the process
|
|
157
|
+
table), waits for `/startupz` to report that version, re-applies Rev4a's config and
|
|
158
|
+
checks the counts. A rollback restores the pre-update backup on the previous version.
|
|
159
|
+
Both run inside the Rev4a process; `markInterruptedUpdates()` at startup turns an
|
|
160
|
+
unfinished one into `interrupted`. `lib/agent-busy.ts` gives the routes one 409 reason
|
|
161
|
+
for "edit, update, recreate, restore or backup running". After a successful update, `pruneAgentImages()`
|
|
162
|
+
keeps only images in use plus the newest and previous versions.
|
|
163
|
+
- **Agent recreates** are `lib/agent-recreate.ts`, with their state in the
|
|
164
|
+
`agent_recreates` table (`lib/agent-recreate-state.ts`): the same-version counterpart
|
|
165
|
+
of an update. A cold backup, then the container rebuilt on the image the agent already
|
|
166
|
+
runs, then `/startupz`, then the runtime config. It answers `202` and runs in the
|
|
167
|
+
background, so a page reload or a Rev4a restart does not lose it; at startup a job
|
|
168
|
+
still active becomes `interrupted` and the agent is started again — after its backup
|
|
169
|
+
helper has finished, never while the archive is written. On success the older
|
|
170
|
+
`prerecreate` archives are pruned to the newest two. The same `recreateAgentContainer()`
|
|
171
|
+
serves the edit route (`PATCH` → 202, no backup) and the Update action.
|
|
172
|
+
`agentBusyReason()` (`lib/agent-busy.ts`) covers it for every route that changes an agent.
|
|
173
|
+
- **Host ports** are shared between creation and the edit flow by `lib/agent-ports.ts`
|
|
174
|
+
(pure: `validatePortInput`, `findAvailablePortBlock`, `portMappingArgs`) and
|
|
175
|
+
`lib/agent-ports-server.ts` (`getUsedHostPorts()`, the Docker lookup, server-only since
|
|
176
|
+
client components cannot import `child_process`). One block maps to the container's
|
|
177
|
+
gateway port 3000 (`3700-3709` → `3700-3709:3000-3009`); both flows produce the same
|
|
178
|
+
validation messages, and the edit excludes the agent's own ports so its block never
|
|
179
|
+
conflicts with itself.
|
|
180
|
+
- **Agent edits** are `lib/agent-edit.ts`, with their state in the `agent_edits` table
|
|
181
|
+
(`lib/agent-edit-state.ts`): the display name (`AGENT_NAME`) and/or the port range are
|
|
182
|
+
applied by rebuilding the container on the image it already runs — **no backup**, the
|
|
183
|
+
volume is never touched. Same 202-and-background shape as a restore; the parameters
|
|
184
|
+
live in the row, so recovery can tell what was being applied. The panel shows a banner
|
|
185
|
+
and the agent card an `EDITING` chip.
|
|
186
|
+
- **Agent restores** are `lib/agent-restore.ts`, with their state in the `agent_restores`
|
|
187
|
+
table (`lib/agent-restore-state.ts`): the archive replaces the volume (stop, clear,
|
|
188
|
+
extract, start), up to 30 minutes. Same 202-and-background shape as a recreate, and the
|
|
189
|
+
same guarantees — a reload or a Rev4a restart does not lose the job, a second restore
|
|
190
|
+
is refused, an interrupted one starts the container again. The extract has no
|
|
191
|
+
percentage (it is a single `tar xzf`); the panel and the agent card show `RESTORING`.
|
|
192
|
+
- **Browser access** to an agent's Control UI goes through `lib/agent-devices.ts`:
|
|
193
|
+
`openclaw devices list | approve | reject | rename | remove` and
|
|
194
|
+
`openclaw dashboard --json`, run inside the container with the async `dockerExec`.
|
|
195
|
+
The gateway token is resolved inside the container (`/root/.agent-token`, then its
|
|
196
|
+
environment), never passed in argv. Whether approval applies is decided by the
|
|
197
|
+
OpenClaw version: 9.x answers `/startupz` with JSON and always requires it;
|
|
198
|
+
2026.7.x follows `gateway.controlUi.dangerouslyDisableDeviceAuth`.
|
|
199
|
+
`buildInviteLink()` builds the plain token link for someone else; the invite route
|
|
200
|
+
refuses agents without approval. Every link's token is read inside the container,
|
|
201
|
+
and its host is the request's `Host` header (`hostnameFromHostHeader()`), never a
|
|
202
|
+
caller-supplied value.
|
|
203
|
+
- **Telegram DM pairing** lives in `lib/channelManager.ts`. The bot binding is config
|
|
204
|
+
(`channels.telegram.botToken`/`dmPolicy`); *who* may talk is OpenClaw's pairing
|
|
205
|
+
store — on 2026.9.3 the SQLite rows in `~/.openclaw/state/openclaw.sqlite`, not the
|
|
206
|
+
`credentials/*.json` files older releases used. Reading and revoking go through the
|
|
207
|
+
store's own functions (`readChannelAllowFromStoreSync`,
|
|
208
|
+
`removeChannelAllowFromStoreEntry`): no CLI, RPC or documented export exposes them,
|
|
209
|
+
so a small script run inside the container finds OpenClaw's `pairing-store` module by
|
|
210
|
+
glob and its functions by name (the names survive minification; no hash or minified
|
|
211
|
+
symbol is hardcoded) and calls them, which uses the same state transaction the CLI
|
|
212
|
+
does. If a release stops exposing the store, the revoke fails with a message pointing
|
|
213
|
+
at `/allowlist remove` — it never reports success without changing anything. Approving
|
|
214
|
+
a pairing request (`openclaw pairing approve`) also bootstraps
|
|
215
|
+
`commands.ownerAllowFrom` for the first owner (OpenClaw's own behaviour).
|
|
122
216
|
|
|
123
217
|
**Running commands inside containers** (`lib/docker-exec.ts`):
|
|
124
218
|
|
|
@@ -136,7 +230,9 @@ the rules for anything you touch, not as a description of the whole tree:
|
|
|
136
230
|
computed for other containers are discarded. Fanning out over a fleet requires
|
|
137
231
|
the `…NoFail` variants, or an explicit per-item try/catch, so one unreachable
|
|
138
232
|
container cannot blank an entire page. Callers: `app/api/skills/route.js`,
|
|
139
|
-
`app/api/
|
|
233
|
+
`app/api/agents/models-summary/route.ts`,
|
|
234
|
+
`app/api/agents/channels-summary/route.ts`,
|
|
235
|
+
`app/api/agents/devices-summary/route.ts`,
|
|
140
236
|
`lib/openclaw-cron.ts`, `app/api/credentials/detect/route.ts`,
|
|
141
237
|
`lib/credentials/delivery.ts`.
|
|
142
238
|
- **Never interpolate a secret into a command string.** Pass it through the `env`
|
|
@@ -183,7 +279,9 @@ agent-{name}-data (named vol) → /root (rw,
|
|
|
183
279
|
All shared volumes are defined centrally in `lib/agent-setup.ts`
|
|
184
280
|
(`REV4A_VOLUMES`) and used by both the create and recreate API routes.
|
|
185
281
|
The `bootstrap-extra-files` hook is pre-wired in the base image Dockerfile
|
|
186
|
-
and guaranteed at runtime by `applyRuntimeConfig()
|
|
282
|
+
and guaranteed at runtime by `applyRuntimeConfig()`, which also applies the Control
|
|
283
|
+
UI browser-origin policy (`withControlUiPolicy()`): the Host-header fallback on, an
|
|
284
|
+
`allowedOrigins: ["*"]` list and the retired `dangerouslyDisableDeviceAuth` removed.
|
|
187
285
|
|
|
188
286
|
**Injected environment variables:**
|
|
189
287
|
```
|
|
@@ -207,7 +305,7 @@ Agent Container Rev4a Gateway Provider API
|
|
|
207
305
|
│ │ │
|
|
208
306
|
│ Authorization: Bearer <gateway-token> │
|
|
209
307
|
│ POST /api/provider/v1/chat/completions │
|
|
210
|
-
│ model: rev4a/deepseek-
|
|
308
|
+
│ model: rev4a/deepseek-flash │
|
|
211
309
|
│────────────────────────>│ │
|
|
212
310
|
│ │ POST /v1/chat/completions│
|
|
213
311
|
│ │ Authorization: Bearer <real-key>
|
|
@@ -325,7 +423,7 @@ Rev4a Dashboard UI
|
|
|
325
423
|
POST /api/agents/create Create new container agent (template + model)
|
|
326
424
|
DELETE /api/agents/{id} Destructive: remove container + volume + backups
|
|
327
425
|
POST /api/agents/{id}/restart Restart container
|
|
328
|
-
POST /api/agents/{id}/recreate Rebuild
|
|
426
|
+
POST /api/agents/{id}/recreate Rebuild on the agent's own version (volume preserved)
|
|
329
427
|
GET /api/agents List all agents (Docker containers with AGENT_ID)
|
|
330
428
|
```
|
|
331
429
|
|
|
@@ -410,7 +508,7 @@ networks:
|
|
|
410
508
|
- Agent → Rev4a Gateway: `http://rev4a-control:3721`
|
|
411
509
|
- Agent → Rev4a Dashboard: `http://rev4a-control:3720`
|
|
412
510
|
- Rev4a → Agent (healthcheck): `http://agent-{id}:3000`
|
|
413
|
-
-
|
|
511
|
+
- Browser → Agent Control UI: the container's published host port, `http://<host>:<port>`. Rev4a sets no reverse-proxy labels
|
|
414
512
|
|
|
415
513
|
---
|
|
416
514
|
|
|
@@ -526,7 +624,7 @@ ALTER TABLE sessions ADD COLUMN ended_at INTEGER;
|
|
|
526
624
|
### Phase 2 — Provider Gateway
|
|
527
625
|
- [x] Implement proxy API on Rev4a (`/api/provider/v1/`)
|
|
528
626
|
- [x] Gateway Page (`/gateway`) for provider key management
|
|
529
|
-
- [x] Agent model configuration
|
|
627
|
+
- [x] Agent model configuration in the agent detail panel
|
|
530
628
|
- [x] Provider sync to agent containers (`PUT /api/gateway/provider`)
|
|
531
629
|
- [x] Auth via rev4a token in `data/provider-keys.json`
|
|
532
630
|
- [ ] Rate limiting per agent
|
|
@@ -561,10 +659,10 @@ ALTER TABLE sessions ADD COLUMN ended_at INTEGER;
|
|
|
561
659
|
- Distributed: per-agent memory, Rev4a indexes centrally — more resilient
|
|
562
660
|
- Recommendation: distributed with central index
|
|
563
661
|
|
|
564
|
-
3. **
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
662
|
+
3. **External access to agents** — decided: each agent's Control UI is published on a
|
|
663
|
+
host port, and Rev4a sets no reverse-proxy labels. HTTPS or a domain means the
|
|
664
|
+
operator's own proxy in front of that port. A first-class integration, with routing
|
|
665
|
+
labels applied at `docker run`, would be a separate feature.
|
|
568
666
|
|
|
569
667
|
4. **Hermes and other frameworks: dashboard integration?**
|
|
570
668
|
- Hermes has its own session format
|
|
@@ -616,6 +714,6 @@ for the full rationale.
|
|
|
616
714
|
| Memory | Per-agent SQLite + central index | New |
|
|
617
715
|
| Config | Generated YAML/JSON | New |
|
|
618
716
|
| Credential stores | SQLite + JSON | credentials.db for service tokens, provider-keys.json for provider API keys. Neither is encrypted |
|
|
619
|
-
| Reverse proxy |
|
|
717
|
+
| Reverse proxy | Optional, operator's choice | Rev4a sets no reverse-proxy labels on agent containers |
|
|
620
718
|
| Monitoring | Rev4a daemon (extended) | Evolution of current daemon |
|
|
621
719
|
| Version control | Git + GitHub | `github.com/Flame0510/rev4a.git` |
|