@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,75 +1,41 @@
1
1
  /**
2
2
  * buildAgentImage.ts
3
3
  *
4
- * Shared logic for obtaining the openclaw-agent-base Docker image.
5
- * Used by /api/agents/download-image, /api/agents/image-status, and
6
- * the rev4a serve CLI.
4
+ * Downloading the agent base image for one OpenClaw version. Used by
5
+ * POST /api/agents/download-image (the Agents page banner) and
6
+ * POST /api/setup/agent-image (the setup wizard, streamed).
7
7
  *
8
- * Strategy (in order):
9
- * 1. docker pull from ghcr.io/flame0510/rev4a/openclaw-agent-base:latest (public, no auth)
10
- * 2. If pull fails (offline / no network) → fallback to local docker build
8
+ * The image is pulled from the registry as `<registry>:<version>` and kept as
9
+ * `openclaw-agent-base:<version>` (see lib/agent-images.ts). When the pull fails and
10
+ * this repository's Dockerfile builds that same version, the image is built locally
11
+ * instead — development without registry access. Any other version cannot be built
12
+ * from here, so its download fails.
11
13
  *
12
- * Logs are written to /tmp/rev4a-download-<timestamp>.log so they survive
13
- * page refreshes and modal closes.
14
+ * One download at a time (in-process lock). Output goes to
15
+ * /tmp/rev4a-download-<timestamp>.log so it survives page refreshes and modal closes.
14
16
  */
15
- import * as crypto from 'crypto';
16
17
  import * as fs from 'fs';
17
18
  import * as path from 'path';
18
- import { execSync as execSyncRaw } from 'child_process';
19
+ import { isSupportedVersion, localImageRef, newestSupportedVersion } from '@/lib/agent-versions';
20
+ import { localVersionExists, pullAgentImage, remoteSupportedVersions, runDocker } from '@/lib/agent-images';
21
+ import { dockerAvailable } from '@/lib/docker-socket';
19
22
 
20
- const REGISTRY_IMAGE = 'ghcr.io/flame0510/rev4a/openclaw-agent-base:latest';
21
- const LOCAL_IMAGE = 'openclaw-agent-base:latest';
23
+ const DOCKERFILE_DIR = path.join(process.cwd(), 'agent-templates', 'base-image');
24
+ const BUILD_TIMEOUT_MS = 60 * 60 * 1000;
22
25
 
23
- let isDownloading = false;
26
+ /** The version being downloaded; `pending` while the default version is being resolved. */
27
+ let downloadingVersion: string | null = null;
24
28
  let downloadLogPath: string | null = null;
25
- let downloadChildProcess: ReturnType<(typeof import('child_process'))['exec']> | null = null;
26
-
27
- /** Check whether the docker CLI is available and functional. */
28
- function dockerAvailable(): boolean {
29
- try {
30
- execSyncRaw('docker version --format "{{.Client.Version}}" 2>/dev/null', {
31
- encoding: 'utf-8',
32
- timeout: 3000,
33
- });
34
- return true;
35
- } catch {
36
- return false;
37
- }
38
- }
39
-
40
- /** Returns the SHA256 hash of the current Dockerfile. */
41
- export function getDockerfileHash(): string | null {
42
- try {
43
- const dockerfile = path.join(process.cwd(), 'agent-templates', 'base-image', 'Dockerfile');
44
- if (!fs.existsSync(dockerfile)) return null;
45
- const content = fs.readFileSync(dockerfile, 'utf-8');
46
- return crypto.createHash('sha256').update(content).digest('hex');
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
29
 
53
30
  /** Checks if a download is already running (in-process flag only). */
54
31
  export function getIsDownloading(): boolean {
55
- return isDownloading;
56
- }
57
-
58
- /** Acquire the lock atomically. Returns false if already locked. */
59
- function tryAcquireLock(): boolean {
60
- if (getIsDownloading()) return false;
61
- isDownloading = true;
62
- return true;
32
+ return downloadingVersion !== null;
63
33
  }
64
34
 
65
- /** Release the lock and clear state. */
66
- function releaseLock(): void {
67
- isDownloading = false;
68
- downloadLogPath = null;
69
- downloadChildProcess = null;
35
+ export function getDownloadingVersion(): string | null {
36
+ return downloadingVersion === 'pending' ? null : downloadingVersion;
70
37
  }
71
38
 
72
-
73
39
  function appendLog(msg: string): void {
74
40
  if (!downloadLogPath) return;
75
41
  try {
@@ -81,270 +47,102 @@ function appendLog(msg: string): void {
81
47
  function cleanupOldLogs(): void {
82
48
  try {
83
49
  const tmpDir = '/tmp';
84
- const files = fs.readdirSync(tmpDir);
85
50
  const cutoff = Date.now() - 24 * 60 * 60 * 1000;
86
- for (const f of files) {
51
+ for (const f of fs.readdirSync(tmpDir)) {
87
52
  if (!f.startsWith('rev4a-download-') || !f.endsWith('.log')) continue;
88
53
  const fullPath = path.join(tmpDir, f);
89
- const stat = fs.statSync(fullPath);
90
- if (stat.mtimeMs < cutoff) {
91
- fs.unlinkSync(fullPath);
92
- }
54
+ if (fs.statSync(fullPath).mtimeMs < cutoff) fs.unlinkSync(fullPath);
93
55
  }
94
56
  } catch { /* ignore */ }
95
57
  }
96
58
 
97
- export interface DownloadOptions {
98
- force?: boolean;
99
- onEvent?: (data: string) => void;
100
- signal?: AbortSignal;
101
- }
102
-
103
- function localImageExists(): boolean {
104
- if (!dockerAvailable()) return false;
59
+ /** The OpenClaw version this repository's Dockerfile builds (`ARG OPENCLAW_VERSION`). */
60
+ export function dockerfileVersion(): string | null {
105
61
  try {
106
- execSyncRaw(
107
- `docker image inspect ${LOCAL_IMAGE} --format "{{.ID}}" 2>/dev/null`,
108
- { timeout: 3000 },
109
- );
110
- return true;
111
- } catch {
112
- return false;
113
- }
114
- }
115
-
116
- function getLocalImageId(): string | null {
117
- if (!dockerAvailable()) return null;
118
- try {
119
- return execSyncRaw(
120
- `docker image inspect ${LOCAL_IMAGE} --format "{{.ID}}" 2>/dev/null`,
121
- { encoding: 'utf-8', timeout: 3000 },
122
- ).trim() || null;
62
+ const content = fs.readFileSync(path.join(DOCKERFILE_DIR, 'Dockerfile'), 'utf-8');
63
+ return /^ARG OPENCLAW_VERSION=(\S+)/m.exec(content)?.[1] ?? null;
123
64
  } catch {
124
65
  return null;
125
66
  }
126
67
  }
127
68
 
128
- /**
129
- * Get the digest of the remote image (without pulling).
130
- * Returns null if not reachable.
131
- */
132
- function getRemoteDigest(): string | null {
133
- if (!dockerAvailable()) return null;
134
- try {
135
- const out = execSyncRaw(
136
- `docker manifest inspect ${REGISTRY_IMAGE} 2>/dev/null`,
137
- { encoding: 'utf-8', timeout: 15000 },
138
- );
139
- const manifest = JSON.parse(out);
140
- return manifest?.config?.digest || null;
141
- } catch {
142
- return null;
143
- }
69
+ /** What a download without an explicit version fetches: the newest supported version the registry publishes, else the newest supported. */
70
+ export async function defaultDownloadVersion(): Promise<string> {
71
+ return (await remoteSupportedVersions())?.[0] ?? newestSupportedVersion();
144
72
  }
145
73
 
146
- /**
147
- * Pull the image from the public registry and tag it locally.
148
- */
149
- async function pullFromRegistry(
150
- opts: DownloadOptions,
151
- emit: (data: string) => void,
152
- ): Promise<boolean> {
153
- if (!dockerAvailable()) {
154
- emit(JSON.stringify({ type: 'error', message: 'Docker is not available' }));
155
- appendLog('Docker not available — cannot download image');
156
- return false;
157
- }
158
-
159
- emit(JSON.stringify({ type: 'status', message: `Downloading ${REGISTRY_IMAGE}...` }));
160
- appendLog(`Downloading from registry: ${REGISTRY_IMAGE}`);
161
-
162
- return new Promise((resolve) => {
163
- const { exec } = require('child_process');
164
- const child = exec(
165
- `docker pull ${REGISTRY_IMAGE}`,
166
- { maxBuffer: 1024 * 1024 * 5, timeout: 300_000 },
167
- (err: Error | null) => {
168
- if (err) {
169
- appendLog(`Download from registry failed: ${err.message}`);
170
- emit(JSON.stringify({ type: 'log', message: 'Registry download failed, trying local build...' }));
171
- resolve(false);
172
- return;
173
- }
174
-
175
- try {
176
- execSyncRaw(`docker tag ${REGISTRY_IMAGE} ${LOCAL_IMAGE}`, { timeout: 10_000 });
177
- } catch { /* ignore tag error */ }
178
- appendLog('Download completed successfully');
179
- emit(JSON.stringify({ type: 'complete', message: 'Image downloaded from registry' }));
180
- resolve(true);
181
- },
182
- );
183
-
184
- downloadChildProcess = child;
185
-
186
- child.stderr?.on('data', (chunk: Buffer) => {
187
- appendLog(chunk.toString());
188
- });
189
-
190
- child.stdout?.on('data', (chunk: Buffer) => {
191
- appendLog(chunk.toString());
192
- });
193
-
194
- child.on('error', (err: Error) => {
195
- appendLog(`Pull process error: ${err.message}`);
196
- // Do NOT release the lock here: the caller falls through to
197
- // localBuild(), which must stay protected. Releasing early let a
198
- // concurrent request past getIsDownloading() and start a second
199
- // `docker build` against the same tag. The lock is released by
200
- // localBuild's own close/error handlers, or by the success path.
201
- resolve(false);
202
- });
203
- });
74
+ export interface DownloadOptions {
75
+ version?: string;
76
+ onEvent?: (data: string) => void;
77
+ signal?: AbortSignal;
204
78
  }
205
79
 
206
- /**
207
- * Local docker build fallback. Returns a Promise.
208
- */
209
- function localBuild(
210
- opts: DownloadOptions,
211
- emit: (data: string) => void,
212
- ): Promise<void> {
213
- return new Promise((resolve, reject) => {
214
- const dockerfileDir = path.join(process.cwd(), 'agent-templates', 'base-image');
215
- const dockerfile = path.join(dockerfileDir, 'Dockerfile');
216
-
217
- if (!fs.existsSync(dockerfile)) {
218
- const msg = 'Dockerfile not found — cannot build locally';
219
- emit(JSON.stringify({ type: 'error', message: msg }));
220
- appendLog(msg);
221
- releaseLock();
222
- reject(new Error(msg));
223
- return;
224
- }
225
-
226
- const hash = getDockerfileHash();
227
- const noCache = opts.force ? '--no-cache ' : '';
228
- const cmd = hash
229
- ? `docker build ${noCache}-t ${LOCAL_IMAGE} --label "rev4a.dockerfile_hash=${hash}" --progress=plain .`
230
- : `docker build ${noCache}-t ${LOCAL_IMAGE} --progress=plain .`;
231
-
232
- appendLog('');
233
- appendLog('=== Local build started (registry download failed) ===');
234
- appendLog(`Dockerfile hash: ${hash || 'unknown'}`);
235
- appendLog('');
236
-
237
- emit(JSON.stringify({ type: 'status', message: 'Building agent base image locally...' }));
238
-
239
- const { exec } = require('child_process');
240
- const child = exec(cmd, {
241
- cwd: dockerfileDir,
242
- maxBuffer: 1024 * 1024 * 10,
243
- });
244
-
245
- downloadChildProcess = child;
246
-
247
- child.stdout?.on('data', (chunk: string) => {
248
- const lines = chunk.toString().split('\n').filter(Boolean);
249
- for (const line of lines) {
250
- appendLog(line);
251
- if (line.startsWith('#')) {
252
- emit(JSON.stringify({ type: 'step', message: line }));
253
- } else if (line.includes('Downloading') || line.includes('Extracting') || line.includes('Pulling')) {
254
- emit(JSON.stringify({ type: 'progress', message: line }));
255
- }
256
- }
257
- });
258
-
259
- child.stderr?.on('data', (chunk: string) => {
260
- const text = chunk.toString().trim();
261
- if (text) {
262
- appendLog(text);
263
- emit(JSON.stringify({ type: 'log', message: text }));
264
- }
265
- });
266
-
267
- child.on('close', (code: number) => {
268
- if (code === 0) {
269
- appendLog('=== Build completed successfully ===');
270
- emit(JSON.stringify({ type: 'complete', message: 'Agent base image built successfully' }));
271
- releaseLock();
272
- resolve();
273
- } else {
274
- appendLog(`=== Build failed with exit code ${code} ===`);
275
- releaseLock();
276
- reject(new Error(`docker build exited with code ${code}`));
277
- }
278
- });
279
-
280
- child.on('error', (err: Error) => {
281
- appendLog(`=== Build error: ${err.message} ===`);
282
- releaseLock();
283
- reject(err);
284
- });
285
- });
80
+ export interface DownloadResult {
81
+ version: string;
82
+ source: 'present' | 'registry' | 'build';
286
83
  }
287
84
 
288
85
  /**
289
- * Main entry point: try pull first, fallback to local build.
86
+ * Download one OpenClaw version of the agent image. Emits JSON events
87
+ * (`status`, `progress`, `step`, `log`, `complete`) through `onEvent`; failures are
88
+ * thrown, not emitted.
290
89
  */
291
- export async function downloadAgentImage(opts: DownloadOptions): Promise<void> {
292
- const { force = false, onEvent, signal } = opts;
293
-
294
- if (!dockerAvailable()) {
295
- const msg = 'Docker is not available — please install Docker and try again';
296
- onEvent?.(JSON.stringify({ type: 'error', message: msg }));
297
- throw new Error(msg);
298
- }
299
-
300
- if (!tryAcquireLock()) {
301
- throw new Error('A download is already in progress');
302
- }
90
+ export async function downloadAgentImage(opts: DownloadOptions = {}): Promise<DownloadResult> {
91
+ const event = (type: string, message: string) => {
92
+ opts.onEvent?.(JSON.stringify({ type, message }));
93
+ appendLog(message);
94
+ };
303
95
 
304
- const timestamp = Date.now();
305
- downloadLogPath = `/tmp/rev4a-download-${timestamp}.log`;
96
+ if (!dockerAvailable()) throw new Error('Docker is not available — please install Docker and try again');
97
+ if (downloadingVersion !== null) throw new Error('A download is already in progress');
98
+ downloadingVersion = opts.version ?? 'pending';
99
+ downloadLogPath = `/tmp/rev4a-download-${Date.now()}.log`;
306
100
 
307
- const emit = (data: string) => {
308
- onEvent?.(data);
309
- try {
310
- const parsed = JSON.parse(data);
311
- if (parsed.message) appendLog(parsed.message);
312
- } catch { /* not JSON */ }
313
- };
101
+ try {
102
+ const version = opts.version ?? (await defaultDownloadVersion());
103
+ if (!isSupportedVersion(version)) throw new Error(`OpenClaw ${version} is not supported by this Rev4a`);
104
+ downloadingVersion = version;
314
105
 
315
- // If not forced and image already exists, check if update is needed
316
- if (!force && localImageExists()) {
317
- const remoteDigest = getRemoteDigest();
318
- if (!remoteDigest) {
319
- emit(JSON.stringify({ type: 'complete', message: 'Image already downloaded (offline, cannot check for updates)' }));
320
- releaseLock();
321
- return;
106
+ if (await localVersionExists(version)) {
107
+ event('complete', `OpenClaw ${version} image already downloaded`);
108
+ return { version, source: 'present' };
322
109
  }
323
110
 
324
- emit(JSON.stringify({ type: 'complete', message: 'Image already downloaded' }));
325
- releaseLock();
326
- return;
327
- }
328
-
329
- // Force: remove local image first so we get a fresh pull
330
- if (force) {
111
+ event('status', `Downloading OpenClaw ${version} image…`);
331
112
  try {
332
- execSyncRaw(`docker rmi -f ${LOCAL_IMAGE} ${REGISTRY_IMAGE} 2>/dev/null || true`, { timeout: 5000 });
333
- } catch { /* ok */ }
334
- }
335
-
336
- appendLog(opts.force ? '=== Image refresh started ===' : '=== Image download started ===');
337
-
338
- // Try download from registry first
339
- const downloaded = await pullFromRegistry(opts, emit);
113
+ await pullAgentImage(version, {
114
+ signal: opts.signal,
115
+ onLine: (line) => {
116
+ appendLog(line);
117
+ if (/Download|Extract|Pull/.test(line)) opts.onEvent?.(JSON.stringify({ type: 'progress', message: line }));
118
+ },
119
+ });
120
+ event('complete', `OpenClaw ${version} image downloaded`);
121
+ return { version, source: 'registry' };
122
+ } catch (e) {
123
+ appendLog(`Registry download failed: ${(e as Error).message}`);
124
+ if (opts.signal?.aborted || dockerfileVersion() !== version) {
125
+ throw new Error(`Could not download OpenClaw ${version}: ${(e as Error).message}`);
126
+ }
127
+ }
340
128
 
341
- if (downloaded) {
129
+ event('log', 'Registry download failed, building the image locally…');
130
+ await runDocker(
131
+ ['build', '--build-arg', `OPENCLAW_VERSION=${version}`, '-t', localImageRef(version), '--progress=plain', DOCKERFILE_DIR],
132
+ {
133
+ signal: opts.signal,
134
+ timeoutMs: BUILD_TIMEOUT_MS,
135
+ onLine: (line) => {
136
+ appendLog(line);
137
+ if (line.startsWith('#')) opts.onEvent?.(JSON.stringify({ type: 'step', message: line }));
138
+ },
139
+ },
140
+ );
141
+ event('complete', `OpenClaw ${version} image built locally`);
142
+ return { version, source: 'build' };
143
+ } finally {
144
+ downloadingVersion = null;
145
+ downloadLogPath = null;
342
146
  cleanupOldLogs();
343
- releaseLock();
344
- return;
345
147
  }
346
-
347
- // Fallback: local build
348
- await localBuild(opts, emit);
349
- cleanupOldLogs();
350
148
  }