@flame0510/project-aether 1.1.9

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 (290) hide show
  1. package/README.md +150 -0
  2. package/agent-templates/README.md +37 -0
  3. package/agent-templates/argus/AGENTS.md +52 -0
  4. package/agent-templates/argus/IDENTITY.md +7 -0
  5. package/agent-templates/argus/MEMORY.md +9 -0
  6. package/agent-templates/argus/SOUL.md +24 -0
  7. package/agent-templates/argus/TOOLS.md +8 -0
  8. package/agent-templates/atlas/AGENTS.md +47 -0
  9. package/agent-templates/atlas/HEARTBEAT.md +10 -0
  10. package/agent-templates/atlas/IDENTITY.md +6 -0
  11. package/agent-templates/atlas/MEMORY.md +9 -0
  12. package/agent-templates/atlas/SOUL.md +21 -0
  13. package/agent-templates/atlas/TOOLS.md +4 -0
  14. package/agent-templates/base-image/.trigger +0 -0
  15. package/agent-templates/base-image/Dockerfile +90 -0
  16. package/agent-templates/base-image/entrypoint.sh +37 -0
  17. package/agent-templates/custom/AGENTS.md +29 -0
  18. package/agent-templates/custom/BOOTSTRAP.md +56 -0
  19. package/agent-templates/custom/MEMORY.md +8 -0
  20. package/agent-templates/prometheus/AGENTS.md +37 -0
  21. package/agent-templates/prometheus/IDENTITY.md +6 -0
  22. package/agent-templates/prometheus/MEMORY.md +9 -0
  23. package/agent-templates/prometheus/SOUL.md +21 -0
  24. package/agent-templates/prometheus/TOOLS.md +4 -0
  25. package/app/agents/ChannelManager.tsx +465 -0
  26. package/app/agents/ImageDownloadBanner.tsx +159 -0
  27. package/app/agents/PageClient.tsx +1285 -0
  28. package/app/agents/create/PageClient.tsx +437 -0
  29. package/app/agents/create/page.tsx +126 -0
  30. package/app/agents/loading.tsx +14 -0
  31. package/app/agents/page.tsx +5 -0
  32. package/app/api/agents/[id]/backup/route.ts +169 -0
  33. package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
  34. package/app/api/agents/[id]/channels/route.ts +14 -0
  35. package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
  36. package/app/api/agents/[id]/lifecycle/route.ts +94 -0
  37. package/app/api/agents/[id]/recreate/route.ts +199 -0
  38. package/app/api/agents/[id]/restart/route.ts +33 -0
  39. package/app/api/agents/[id]/restore/route.ts +81 -0
  40. package/app/api/agents/[id]/route.ts +280 -0
  41. package/app/api/agents/channels-summary/route.ts +39 -0
  42. package/app/api/agents/config-json/route.ts +58 -0
  43. package/app/api/agents/create/route.ts +488 -0
  44. package/app/api/agents/download-image/route.ts +32 -0
  45. package/app/api/agents/image-status/route.ts +126 -0
  46. package/app/api/agents/route.ts +213 -0
  47. package/app/api/agents/token/route.ts +94 -0
  48. package/app/api/agents-active/route.ts +190 -0
  49. package/app/api/agents-config/route.ts +629 -0
  50. package/app/api/alerts/smoke/route.ts +26 -0
  51. package/app/api/aliases/add/route.ts +28 -0
  52. package/app/api/aliases/remove/route.ts +27 -0
  53. package/app/api/assistant/route.ts +204 -0
  54. package/app/api/auth/check/route.ts +38 -0
  55. package/app/api/auth/login/route.ts +85 -0
  56. package/app/api/auth/logout/route.ts +7 -0
  57. package/app/api/auth/status/route.ts +33 -0
  58. package/app/api/config/env/route.ts +132 -0
  59. package/app/api/config/restart/route.ts +26 -0
  60. package/app/api/containers/route.ts +75 -0
  61. package/app/api/cost-override/route.ts +46 -0
  62. package/app/api/costs/route.ts +84 -0
  63. package/app/api/credentials/[id]/reveal/route.ts +21 -0
  64. package/app/api/credentials/[id]/route.ts +52 -0
  65. package/app/api/credentials/[id]/sync/route.ts +58 -0
  66. package/app/api/credentials/detect/route.ts +246 -0
  67. package/app/api/credentials/route.ts +33 -0
  68. package/app/api/crons/[id]/route.ts +69 -0
  69. package/app/api/crons/route.ts +12 -0
  70. package/app/api/crons/runs/route.ts +31 -0
  71. package/app/api/debug/route.ts +11 -0
  72. package/app/api/envcheck/route.ts +18 -0
  73. package/app/api/events/route.ts +24 -0
  74. package/app/api/gateway/agent/route.ts +154 -0
  75. package/app/api/gateway/provider/balance/route.ts +38 -0
  76. package/app/api/gateway/provider/keys.ts +38 -0
  77. package/app/api/gateway/provider/oauth/route.ts +304 -0
  78. package/app/api/gateway/provider/route.ts +209 -0
  79. package/app/api/gateway/route.ts +223 -0
  80. package/app/api/gateway/sync.ts +189 -0
  81. package/app/api/lineage/route.ts +54 -0
  82. package/app/api/memory-context/route.ts +15 -0
  83. package/app/api/metrics/route.ts +35 -0
  84. package/app/api/models/route.ts +77 -0
  85. package/app/api/plugins/route.js +37 -0
  86. package/app/api/provider/upstream.ts +142 -0
  87. package/app/api/provider/v1/chat/completions/route.ts +146 -0
  88. package/app/api/provider/v1/models/route.ts +167 -0
  89. package/app/api/provider-usage/route.ts +629 -0
  90. package/app/api/session/route.ts +39 -0
  91. package/app/api/sessions/[id]/route.ts +38 -0
  92. package/app/api/sessions/route.ts +23 -0
  93. package/app/api/setup/agent-image/route.ts +102 -0
  94. package/app/api/setup/password/route.ts +111 -0
  95. package/app/api/setup/restart/route.ts +76 -0
  96. package/app/api/skills/delete/route.js +105 -0
  97. package/app/api/skills/promote/route.js +129 -0
  98. package/app/api/skills/route.js +203 -0
  99. package/app/api/skills/save/route.js +142 -0
  100. package/app/api/stats/route.ts +34 -0
  101. package/app/api/stats-since/route.ts +20 -0
  102. package/app/api/stream/route.ts +70 -0
  103. package/app/api/system-health/route.ts +233 -0
  104. package/app/api/tool-calls/route.ts +24 -0
  105. package/app/api/tools-config/route.ts +163 -0
  106. package/app/api/update-check/route.ts +127 -0
  107. package/app/api/version/route.ts +25 -0
  108. package/app/api/wizard/complete/route.ts +32 -0
  109. package/app/api/wizard/reset/route.ts +28 -0
  110. package/app/api/wizard/status/route.ts +28 -0
  111. package/app/api/workspace/route.ts +392 -0
  112. package/app/api/workspace/stream/route.ts +136 -0
  113. package/app/components/AppShell.tsx +33 -0
  114. package/app/components/AuthGuard.tsx +91 -0
  115. package/app/components/CostBreakdown.tsx +37 -0
  116. package/app/components/DashboardHeader.tsx +73 -0
  117. package/app/components/DashboardLayout.tsx +76 -0
  118. package/app/components/DashboardToolbar.tsx +61 -0
  119. package/app/components/FloatingActions.tsx +33 -0
  120. package/app/components/Icons.tsx +78 -0
  121. package/app/components/LineageGraphPage.tsx +142 -0
  122. package/app/components/LiveFeed.tsx +48 -0
  123. package/app/components/MobileBottomNav.tsx +84 -0
  124. package/app/components/ModelPickerModal.tsx +94 -0
  125. package/app/components/PasswordInput.tsx +86 -0
  126. package/app/components/PulseChat.tsx +362 -0
  127. package/app/components/Rev4aLoader.tsx +48 -0
  128. package/app/components/SessionDrawer.tsx +263 -0
  129. package/app/components/SessionTopology.tsx +289 -0
  130. package/app/components/Sidebar.tsx +294 -0
  131. package/app/components/Skeleton.tsx +133 -0
  132. package/app/components/SystemCockpit.tsx +169 -0
  133. package/app/components/VersionBanner.tsx +105 -0
  134. package/app/components/WizardButton.tsx +30 -0
  135. package/app/components/ui/Badge.tsx +30 -0
  136. package/app/components/ui/Button.tsx +59 -0
  137. package/app/components/ui/ConfirmModal.tsx +46 -0
  138. package/app/components/ui/FilterBar.tsx +33 -0
  139. package/app/components/ui/Input.tsx +25 -0
  140. package/app/components/ui/ItemList.tsx +83 -0
  141. package/app/components/ui/LoadingSpinner.tsx +30 -0
  142. package/app/components/ui/Metric.tsx +22 -0
  143. package/app/components/ui/Modal.tsx +42 -0
  144. package/app/components/ui/Page.tsx +18 -0
  145. package/app/components/ui/Pill.tsx +6 -0
  146. package/app/components/ui/PropertyList.tsx +24 -0
  147. package/app/components/ui/Select.tsx +26 -0
  148. package/app/components/ui/StatusCard.tsx +32 -0
  149. package/app/components/ui/Surface.tsx +14 -0
  150. package/app/components/ui/Tabs.tsx +55 -0
  151. package/app/components/ui/TemplateOption.tsx +59 -0
  152. package/app/components/ui/Toast.tsx +44 -0
  153. package/app/components/ui/index.ts +19 -0
  154. package/app/components/ui/tokens.ts +18 -0
  155. package/app/config/PageClient.tsx +293 -0
  156. package/app/config/loading.tsx +10 -0
  157. package/app/config/page.tsx +7 -0
  158. package/app/containers/ContainersClient.tsx +129 -0
  159. package/app/containers/loading.tsx +21 -0
  160. package/app/containers/page.tsx +5 -0
  161. package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
  162. package/app/containers/terminal/[id]/page.tsx +6 -0
  163. package/app/credentials/PageClient.tsx +598 -0
  164. package/app/credentials/loading.tsx +10 -0
  165. package/app/credentials/page.tsx +7 -0
  166. package/app/crons/PageClient.tsx +538 -0
  167. package/app/crons/loading.tsx +10 -0
  168. package/app/crons/page.tsx +5 -0
  169. package/app/design-system.ts +57 -0
  170. package/app/gateway/PageClient.tsx +1048 -0
  171. package/app/gateway/loading.tsx +24 -0
  172. package/app/gateway/page.tsx +5 -0
  173. package/app/globals.css +1474 -0
  174. package/app/layout.tsx +45 -0
  175. package/app/lib/models-context.tsx +41 -0
  176. package/app/lineage/loading.tsx +10 -0
  177. package/app/lineage/page.tsx +13 -0
  178. package/app/loading.tsx +16 -0
  179. package/app/login/page.tsx +160 -0
  180. package/app/memory/MemoryContextPageClient.tsx +196 -0
  181. package/app/memory/loading.tsx +10 -0
  182. package/app/memory/page.tsx +8 -0
  183. package/app/page.tsx +31 -0
  184. package/app/plugins/PageClient.tsx +296 -0
  185. package/app/plugins/loading.tsx +10 -0
  186. package/app/plugins/page.tsx +5 -0
  187. package/app/setup/PageClient.tsx +227 -0
  188. package/app/setup/loading.tsx +21 -0
  189. package/app/setup/page.tsx +7 -0
  190. package/app/skills/PageClient.tsx +499 -0
  191. package/app/skills/loading.tsx +10 -0
  192. package/app/skills/page.tsx +5 -0
  193. package/app/tools/ToolsPageClient.tsx +308 -0
  194. package/app/tools/loading.tsx +10 -0
  195. package/app/tools/page.tsx +62 -0
  196. package/app/tools/tools-catalog.ts +141 -0
  197. package/app/wizard/PageClient.tsx +485 -0
  198. package/app/wizard/icons.tsx +89 -0
  199. package/app/wizard/loading.tsx +24 -0
  200. package/app/wizard/page.tsx +7 -0
  201. package/app/wizard/storage.ts +33 -0
  202. package/app/wizard/useWizard.ts +101 -0
  203. package/app/workspace/WorkspaceClient.tsx +633 -0
  204. package/app/workspace/loading.tsx +36 -0
  205. package/app/workspace/page.tsx +8 -0
  206. package/bin/docker-entrypoint.sh +21 -0
  207. package/bin/postinstall.js +20 -0
  208. package/bin/rev4a.js +696 -0
  209. package/daemon.js +783 -0
  210. package/docs/ARCHITECTURE.md +557 -0
  211. package/docs/CONTAINER-TERMINAL.md +280 -0
  212. package/docs/DESIGN-SYSTEM.md +99 -0
  213. package/docs/FRONTEND-ARCHITECTURE.md +119 -0
  214. package/docs/REV4A.md +504 -0
  215. package/docs/dev/API-REFERENCE.md +1835 -0
  216. package/docs/dev/DATABASE.md +224 -0
  217. package/docs/dev/GATEWAY.md +300 -0
  218. package/docs/dev/PROVIDERS.md +229 -0
  219. package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
  220. package/docs/dev/WORKSPACE.md +199 -0
  221. package/docs/rag/DATA-FRESHNESS.md +109 -0
  222. package/docs/rag/GLOSSARY.md +125 -0
  223. package/docs/rag/REV4A-OVERVIEW.md +135 -0
  224. package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
  225. package/lib/agent-setup.ts +130 -0
  226. package/lib/alerts.ts +115 -0
  227. package/lib/apiFetch.ts +7 -0
  228. package/lib/auth.ts +28 -0
  229. package/lib/billing.ts +100 -0
  230. package/lib/buildAgentImage.ts +346 -0
  231. package/lib/channelManager.ts +386 -0
  232. package/lib/container.ts +60 -0
  233. package/lib/credentials/db.ts +86 -0
  234. package/lib/credentials/delivery.ts +448 -0
  235. package/lib/credentials/detect.ts +149 -0
  236. package/lib/credentials/hash.ts +9 -0
  237. package/lib/credentials/providers.ts +110 -0
  238. package/lib/credentials/vault.ts +195 -0
  239. package/lib/db-bootstrap.d.ts +3 -0
  240. package/lib/db-bootstrap.mjs +144 -0
  241. package/lib/db.ts +25 -0
  242. package/lib/docker-utils.ts +60 -0
  243. package/lib/hooks/useDashboard.ts +264 -0
  244. package/lib/hooks/useRev4aTimezone.ts +28 -0
  245. package/lib/hooks/useScreenshot.ts +30 -0
  246. package/lib/memory-context.ts +385 -0
  247. package/lib/model-catalogue.ts +131 -0
  248. package/lib/model-pricing.ts +63 -0
  249. package/lib/openclaw-cron.ts +350 -0
  250. package/lib/patterns/ApiAdapter.ts +73 -0
  251. package/lib/patterns/ApiClient.ts +65 -0
  252. package/lib/patterns/EventBus.ts +113 -0
  253. package/lib/patterns/FilterStrategy.ts +79 -0
  254. package/lib/patterns/SessionFactory.ts +117 -0
  255. package/lib/patterns/sessionPresentation.ts +117 -0
  256. package/lib/provider-balance.ts +163 -0
  257. package/lib/requireAuth.tsx +38 -0
  258. package/lib/rev4a-auth.d.ts +3 -0
  259. package/lib/rev4a-auth.js +36 -0
  260. package/lib/rev4a-paths.ts +145 -0
  261. package/lib/timezone.ts +46 -0
  262. package/lib/types/index.ts +96 -0
  263. package/lib/utils/format.ts +82 -0
  264. package/lineage.js +56 -0
  265. package/model-pricing.json +482 -0
  266. package/models.config.json +844 -0
  267. package/next-env.d.ts +6 -0
  268. package/next.config.mjs +7 -0
  269. package/package.json +82 -0
  270. package/public/apple-touch-icon.png +0 -0
  271. package/public/favicon-64.png +0 -0
  272. package/public/favicon.svg +58 -0
  273. package/public/icon-192-maskable.png +0 -0
  274. package/public/icon-192.png +0 -0
  275. package/public/icon-512-maskable.png +0 -0
  276. package/public/icon-512.png +0 -0
  277. package/public/manifest.json +38 -0
  278. package/public/rev4a-logo.png +0 -0
  279. package/public/rev4a-logo.svg +58 -0
  280. package/rev4a-rules/AGENTS.md +37 -0
  281. package/scripts/add-auth.cjs +185 -0
  282. package/scripts/backup.sh +66 -0
  283. package/scripts/check-providers.mjs +143 -0
  284. package/scripts/docker/traefik/docker-compose.yml +32 -0
  285. package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
  286. package/scripts/docker/traefik/traefik.yml +37 -0
  287. package/scripts/remove-auth-profile.js +56 -0
  288. package/scripts/restore.sh +82 -0
  289. package/terminal-ws-server.js +139 -0
  290. package/tsconfig.json +42 -0
@@ -0,0 +1,81 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { execSync } from 'child_process';
3
+ import { resolveAgentedContainer, isValidAgentId } from '@/lib/container';
4
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
5
+
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ const BACKUP_VOLUME = 'rev4a-backups';
9
+
10
+ /**
11
+ * POST /api/agents/[id]/restore?file=agent-xxx-2026-07-11_2115.tar.gz
12
+ * Restore an agent's persistent volume from a backup.
13
+ * Stops the container, restores the volume, then restarts.
14
+ */
15
+ export async function POST(request: Request,
16
+ { params }: { params: Promise<{ id: string }> },
17
+ ): Promise<NextResponse> {
18
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
19
+ try {
20
+ const { id } = await params;
21
+ if (!id) return NextResponse.json({ error: 'Agent name required' }, { status: 400 });
22
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
23
+
24
+ const url = new URL(request.url);
25
+ const file = url.searchParams.get('file');
26
+ if (!file) return NextResponse.json({ error: 'file query param required' }, { status: 400 });
27
+
28
+ // Safety: only allow restoring agent-specific files, reject path traversal
29
+ if (!file.startsWith(`agent-${id}-`) || !file.endsWith('.tar.gz') || file.includes('/') || file.includes('..')) {
30
+ return NextResponse.json({ error: 'Invalid backup file name' }, { status: 400 });
31
+ }
32
+
33
+ const volume = `agent-${id}-data`;
34
+
35
+ // Resolve the actual container name via AGENT_ID label
36
+ const containerName = resolveAgentedContainer(id);
37
+
38
+ // Check backup exists
39
+ const check = execSync(
40
+ `docker run --rm -v ${BACKUP_VOLUME}:/backup alpine sh -c 'test -f /backup/${file} && echo ok || true'`,
41
+ { timeout: 10000, encoding: 'utf-8' },
42
+ ).trim();
43
+ if (check !== 'ok') {
44
+ return NextResponse.json({ error: `Backup file '${file}' not found` }, { status: 404 });
45
+ }
46
+
47
+ // Stop the container if running
48
+ if (containerName) {
49
+ execSync(`docker stop ${containerName} 2>/dev/null; exit 0`, { timeout: 15000 });
50
+ }
51
+
52
+ // Clear the volume and restore from backup.
53
+ // Run a temporary container with the volume, delete ALL contents (including
54
+ // dotfiles/dotdirs like .openclaw, .config, .local — the .[!.]* glob is
55
+ // required, otherwise the restore only merges on top of stale state), then
56
+ // extract the backup. Always restart the container afterwards, even on failure.
57
+ try {
58
+ execSync(
59
+ `docker run --rm ` +
60
+ `-v ${volume}:/target ` +
61
+ `-v ${BACKUP_VOLUME}:/backup ` +
62
+ `alpine sh -c 'rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null; tar xzf /backup/${file} -C /target'`,
63
+ { timeout: 30000, stdio: 'pipe' },
64
+ );
65
+ } finally {
66
+ // Restart the container regardless of extract outcome so it never stays down.
67
+ if (containerName) {
68
+ try {
69
+ execSync(`docker start ${containerName}`, { timeout: 15000, stdio: 'pipe' });
70
+ } catch {
71
+ // container start failure is surfaced separately; nothing else to do here
72
+ }
73
+ }
74
+ }
75
+
76
+ return NextResponse.json({ success: true, container: containerName || id });
77
+ } catch (e: unknown) {
78
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
79
+ }
80
+ }
81
+
@@ -0,0 +1,280 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { execSync } from 'child_process';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { resolveAgentedContainer, isValidAgentId } from '@/lib/container';
6
+ import { buildRev4aProviderConfig } from '@/app/api/gateway/sync';
7
+ import { getMountFlags, applyRuntimeConfig } from '@/lib/agent-setup';
8
+ import { DB_FILE } from '@/lib/rev4a-paths';
9
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
10
+
11
+ const BACKUP_VOLUME = 'rev4a-backups';
12
+
13
+ export const dynamic = 'force-dynamic';
14
+
15
+ export async function DELETE(request: Request,
16
+ { params }: { params: Promise<{ id: string }> },
17
+ ): Promise<NextResponse> {
18
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
19
+ try {
20
+ const { id } = await params;
21
+
22
+ if (!id || id.length < 1) {
23
+ return NextResponse.json(
24
+ { success: false, error: 'Container name or ID is required' },
25
+ { status: 400 },
26
+ );
27
+ }
28
+ if (!isValidAgentId(id)) {
29
+ return NextResponse.json({ success: false, error: 'Invalid agent id' }, { status: 400 });
30
+ }
31
+
32
+ // Only allow deletion of containers that have a matching AGENT_ID label
33
+ const containerName = resolveAgentedContainer(id);
34
+ if (!containerName) {
35
+ return NextResponse.json({ error: `No container found with AGENT_ID '${id}'` }, { status: 404 });
36
+ }
37
+
38
+ execSync(`docker rm -f "${containerName}"`, {
39
+ encoding: 'utf-8',
40
+ timeout: 30000,
41
+ });
42
+
43
+ // Destructive delete: also remove the persistent volume and all backups.
44
+ // The container is already gone, so the volume is no longer in use.
45
+ // Best-effort — a missing volume or absent backups must not fail the delete.
46
+ try {
47
+ execSync(`docker volume rm agent-${id}-data`, { timeout: 15000, stdio: 'pipe' });
48
+ } catch {
49
+ // volume may not exist (legacy agent) or already removed — ignore
50
+ }
51
+ try {
52
+ execSync(
53
+ `docker run --rm -v ${BACKUP_VOLUME}:/backup alpine sh -c 'rm -f /backup/agent-${id}-*.tar.gz'`,
54
+ { timeout: 20000, stdio: 'pipe' },
55
+ );
56
+ } catch {
57
+ // no backups or backup volume missing — ignore
58
+ }
59
+
60
+ // Log event to events.db if it exists
61
+ try {
62
+ const Database = require('better-sqlite3');
63
+ const dbPath = DB_FILE;
64
+ if (fs.existsSync(dbPath)) {
65
+ const db = new Database(dbPath, { readonly: false });
66
+ db.pragma('journal_mode = WAL');
67
+ db.prepare(
68
+ `INSERT INTO events (ts, session_id, type, data) VALUES (?, ?, ?, ?)`,
69
+ ).run(Date.now(), `system:agents`, 'agent_deleted', JSON.stringify({
70
+ name: id,
71
+ containerName,
72
+ }));
73
+ db.close();
74
+ }
75
+ } catch {
76
+ // DB logging is optional, ignore failures
77
+ }
78
+
79
+ return NextResponse.json({ success: true });
80
+ } catch (e: unknown) {
81
+ const err = e as Error;
82
+ return NextResponse.json(
83
+ { success: false, error: err.message || 'Unknown error' },
84
+ { status: 500 },
85
+ );
86
+ }
87
+ }
88
+
89
+ function waitForContainerReady(name: string, timeoutMs = 60000): boolean {
90
+ const start = Date.now();
91
+ while (Date.now() - start < timeoutMs) {
92
+ try {
93
+ execSync(
94
+ `docker exec ${name} sh -c 'curl -sf http://127.0.0.1:3000/health >/dev/null 2>&1 || wget -qO- http://127.0.0.1:3000/health >/dev/null 2>&1'`,
95
+ { timeout: 5000 },
96
+ );
97
+ return true;
98
+ } catch { /* not ready */ }
99
+ execSync('sleep 2', { timeout: 3000 });
100
+ }
101
+ return false;
102
+ }
103
+
104
+ function shellQuote(value: string): string {
105
+ return `'${value.replace(/'/g, `'\\''`)}'`;
106
+ }
107
+
108
+ /**
109
+ * PATCH /api/agents/[id]
110
+ *
111
+ * Update agent settings (display name, ports — future).
112
+ * Recreates the container with updated env vars while preserving the
113
+ * persistent volume, workspace, sessions, and all data.
114
+ */
115
+ export async function PATCH(
116
+ request: Request,
117
+ { params }: { params: Promise<{ id: string }> },
118
+ ): Promise<NextResponse> {
119
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
120
+ try {
121
+ const { id } = await params;
122
+ if (!id) return NextResponse.json({ error: 'Agent ID required' }, { status: 400 });
123
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
124
+
125
+ let body: { displayName?: string; portRange?: string };
126
+ try {
127
+ body = await request.json();
128
+ } catch {
129
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
130
+ }
131
+
132
+ const { displayName, portRange } = body;
133
+ if (!displayName && !portRange) {
134
+ return NextResponse.json({ error: 'No fields to update' }, { status: 400 });
135
+ }
136
+
137
+ if (displayName !== undefined && (!displayName || displayName.trim().length < 1)) {
138
+ return NextResponse.json({ error: 'Display name must not be empty' }, { status: 400 });
139
+ }
140
+
141
+ // Resolve the container
142
+ const containerName = resolveAgentedContainer(id);
143
+ if (!containerName) {
144
+ return NextResponse.json({ error: `No container found with AGENT_ID '${id}'` }, { status: 404 });
145
+ }
146
+
147
+ // Inspect existing container
148
+ let inspect: string;
149
+ try {
150
+ inspect = execSync(`docker inspect ${containerName}`, { timeout: 10000, encoding: 'utf-8' }).trim();
151
+ } catch {
152
+ return NextResponse.json({ error: `Container '${containerName}' not found` }, { status: 404 });
153
+ }
154
+
155
+ const info = JSON.parse(inspect);
156
+ const [container] = info;
157
+ const image = container.Config?.Image || 'openclaw-agent-base:latest';
158
+ const currentEnv: string[] = container.Config?.Env || [];
159
+
160
+ // Update AGENT_NAME in env vars
161
+ let env = [...currentEnv];
162
+ if (displayName) {
163
+ const idx = env.findIndex((e: string) => e.startsWith('AGENT_NAME='));
164
+ const newEntry = `AGENT_NAME=${displayName.trim()}`;
165
+ if (idx >= 0) {
166
+ env[idx] = newEntry;
167
+ } else {
168
+ env.push(newEntry);
169
+ }
170
+ }
171
+
172
+ // Keep only relevant env vars
173
+ const keepPrefixes = ['AGENT_', 'MODEL_', 'OPENCLAW_', 'TZ='];
174
+ const envOpts = env
175
+ .filter((e: string) => keepPrefixes.some((p) => e.startsWith(p)))
176
+ .map((e: string) => `-e ${shellQuote(e)}`)
177
+ .join(' ');
178
+
179
+ // Labels
180
+ const labels = container.Config?.Labels || {};
181
+ const keepLabelKeys = ['AGENT_ID', 'traefik.', 'description', 'maintainer'];
182
+ const labelOpts = Object.entries(labels)
183
+ .filter(([k]) => keepLabelKeys.some((p) => k === p || k.startsWith(p)))
184
+ .map(([k, v]) => `-l ${shellQuote(`${k}=${v}`)}`)
185
+ .join(' ');
186
+
187
+ // Network
188
+ const networks = container.NetworkSettings?.Networks || {};
189
+ const network = Object.keys(networks)[0];
190
+ if (!network) {
191
+ return NextResponse.json({ error: 'Cannot determine network' }, { status: 500 });
192
+ }
193
+
194
+ // Port mappings
195
+ const portBindings = (container.HostConfig?.PortBindings || {}) as Record<string, { HostPort: string }[]>;
196
+ let portOpts = Object.entries(portBindings)
197
+ .map(([containerPort, bindings]) => {
198
+ const hostPort = bindings[0]?.HostPort;
199
+ if (!hostPort) return '';
200
+ const proto = containerPort.includes('/udp') ? '/udp' : '';
201
+ return `-p ${hostPort}:${containerPort.replace(/\/.*$/, '')}${proto}`;
202
+ })
203
+ .filter(Boolean)
204
+ .join(' ');
205
+
206
+ // If portRange is provided, replace the port mapping
207
+ if (portRange) {
208
+ const parts = portRange.trim().split('-');
209
+ const start = parseInt(parts[0], 10);
210
+ const end = parts[1] ? parseInt(parts[1], 10) : start;
211
+ if (isNaN(start) || isNaN(end) || start < 1 || end > 65535 || end < start) {
212
+ return NextResponse.json({ error: 'Invalid port range' }, { status: 400 });
213
+ }
214
+ if (end === start) {
215
+ portOpts = `-p ${start}:3000`;
216
+ } else {
217
+ portOpts = `-p ${start}-${end}:3000-${3000 + (end - start)}`;
218
+ }
219
+ }
220
+
221
+ const volumeName = `agent-${id}-data`;
222
+
223
+ // Stop & remove old container (keep volume)
224
+ try {
225
+ execSync(`docker rm -f ${containerName}`, { timeout: 15000 });
226
+ } catch (rmErr) {
227
+ return NextResponse.json(
228
+ { error: `Failed to remove old container: ${(rmErr as Error).message}` },
229
+ { status: 500 },
230
+ );
231
+ }
232
+
233
+ // Run new container with same volume
234
+ const cmd = [
235
+ `docker run -d`,
236
+ `--name "${id}"`,
237
+ `--network "${network}"`,
238
+ `--restart unless-stopped`,
239
+ `--add-host host.docker.internal:host-gateway`,
240
+ `-v ${volumeName}:/root`,
241
+ ...getMountFlags(),
242
+ labelOpts,
243
+ envOpts,
244
+ portOpts,
245
+ `"${image}"`,
246
+ ].filter(Boolean).join(' \\\n ');
247
+
248
+ execSync(cmd, { timeout: 60000, encoding: 'utf-8' });
249
+
250
+ // Wait for healthy
251
+ const ready = waitForContainerReady(id);
252
+ if (!ready) {
253
+ return NextResponse.json(
254
+ { error: 'Container started but gateway did not become healthy within 60s.' },
255
+ { status: 500 },
256
+ );
257
+ }
258
+
259
+ // Apply runtime config
260
+ try {
261
+ applyRuntimeConfig(id);
262
+ const providerConfig = buildRev4aProviderConfig();
263
+ const providerJson = JSON.stringify({ models: { providers: { rev4a: providerConfig } } });
264
+ execSync(
265
+ `docker exec -i ${id} sh -c 'openclaw config patch --stdin'`,
266
+ { timeout: 15000, stdio: 'pipe', input: providerJson },
267
+ );
268
+ try { execSync(`docker exec ${id} openclaw gateway restart`, { timeout: 15000 }); } catch { /* non-fatal */ }
269
+ } catch { /* non-fatal */ }
270
+
271
+ return NextResponse.json({
272
+ success: true,
273
+ displayName: displayName?.trim() || null,
274
+ });
275
+ } catch (e: unknown) {
276
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
277
+ }
278
+ }
279
+
280
+
@@ -0,0 +1,39 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { getChannels } from '@/lib/channelManager';
3
+ import { execSync } from 'child_process';
4
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
5
+
6
+ export const dynamic = 'force-dynamic';
7
+ export const maxDuration = 15; // 15-second timeout for channels summary
8
+
9
+ /**
10
+ * GET /api/agents/channels-summary
11
+ *
12
+ * Returns a lightweight channel status map for all running agents.
13
+ * { [agentId]: { telegram: boolean } }
14
+ */
15
+ export async function GET(request: NextRequest,
16
+ ): Promise<NextResponse> {
17
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
18
+ try {
19
+ // Discover agents with AGENT_ID label
20
+ const out = execSync(
21
+ `docker ps --filter "label=AGENT_ID" --format '{{.Label "AGENT_ID"}}' 2>/dev/null`,
22
+ { encoding: 'utf-8', timeout: 5000 },
23
+ ).trim();
24
+
25
+ const agentIds = out ? out.split('\n').filter(Boolean) : [];
26
+ const summary: Record<string, { telegram: boolean }> = {};
27
+
28
+ for (const agentId of agentIds) {
29
+ const channels = getChannels(agentId);
30
+ summary[agentId] = {
31
+ telegram: channels.telegram?.connected === true,
32
+ };
33
+ }
34
+
35
+ return NextResponse.json(summary);
36
+ } catch {
37
+ return NextResponse.json({});
38
+ }
39
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * GET /api/agents/config-json?name=<container>
3
+ *
4
+ * Returns the full content of openclaw.json from a container.
5
+ * Used by the Agents page to inspect container config without SSH.
6
+ */
7
+ import { NextRequest, NextResponse } from 'next/server';
8
+ import { execSync } from 'child_process';
9
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
10
+
11
+ export const dynamic = 'force-dynamic';
12
+ export const runtime = 'nodejs';
13
+
14
+ export async function GET(request: NextRequest) {
15
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
16
+ try {
17
+ const name = request.nextUrl.searchParams.get('name');
18
+ if (!name) {
19
+ return NextResponse.json({ error: 'name query param is required' }, { status: 400 });
20
+ }
21
+
22
+ // Sanitize
23
+ const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, '');
24
+ if (safeName !== name) {
25
+ return NextResponse.json({ error: 'Invalid container name' }, { status: 400 });
26
+ }
27
+
28
+ const raw = execSync(
29
+ `docker exec ${safeName} cat /root/.openclaw/openclaw.json 2>/dev/null || true`,
30
+ { timeout: 8000, encoding: 'utf-8', maxBuffer: 512 * 1024 },
31
+ ).trim();
32
+
33
+ if (!raw) {
34
+ return NextResponse.json({ error: 'Config file not found or empty in container' }, { status: 404 });
35
+ }
36
+
37
+ let parsed: Record<string, unknown>;
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ } catch {
41
+ return NextResponse.json({
42
+ error: 'Failed to parse config JSON',
43
+ content: raw.slice(0, 500),
44
+ }, { status: 500 });
45
+ }
46
+
47
+ // Sanitize: mask tokens in the response
48
+ const sanitized = JSON.parse(JSON.stringify(parsed));
49
+ if (sanitized.gateway?.auth?.token) {
50
+ sanitized.gateway.auth.token = '***';
51
+ }
52
+
53
+ return NextResponse.json({ name: safeName, config: sanitized });
54
+ } catch (e: unknown) {
55
+ const message = e instanceof Error ? e.message : String(e);
56
+ return NextResponse.json({ error: message }, { status: 500 });
57
+ }
58
+ }