@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,169 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { execSync } from 'child_process';
3
+ import { 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
+ * Determine the volume name for an agent by checking if it exists.
12
+ */
13
+ function agentVolumeName(agentName: string): string | null {
14
+ try {
15
+ const vols = execSync(
16
+ "docker volume ls --format '{{.Name}}'",
17
+ { encoding: 'utf-8', timeout: 5000 },
18
+ ).trim().split('\n').filter(Boolean);
19
+ const needle = `agent-${agentName}-data`;
20
+ return vols.includes(needle) ? needle : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /** Format a timestamp as YYYY-MM-DD_HHmmssSSS (ms precision avoids same-second collisions). */
27
+ function ts(): string {
28
+ const d = new Date();
29
+ const pad = (n: number) => String(n).padStart(2, '0');
30
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
31
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}${ms}`;
32
+ }
33
+
34
+ /** Convert bytes to human-readable size. */
35
+ function fmtSize(bytes: number): string {
36
+ if (bytes < 1024) return `${bytes} B`;
37
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
38
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
39
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
40
+ }
41
+
42
+ /**
43
+ * POST /api/agents/[id]/backup
44
+ * Create a backup of the agent's persistent volume.
45
+ */
46
+ export async function POST(request: Request,
47
+ { params }: { params: Promise<{ id: string }> },
48
+ ): Promise<NextResponse> {
49
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
50
+ try {
51
+ const { id } = await params;
52
+ if (!id) return NextResponse.json({ error: 'Agent name required' }, { status: 400 });
53
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
54
+
55
+ const volume = agentVolumeName(id);
56
+ if (!volume) {
57
+ return NextResponse.json({ error: `No persistent volume found for agent '${id}'` }, { status: 404 });
58
+ }
59
+
60
+ const filename = `agent-${id}-${ts()}.tar.gz`;
61
+
62
+ execSync(
63
+ `docker run --rm ` +
64
+ `-v ${volume}:/source:ro ` +
65
+ `-v ${BACKUP_VOLUME}:/backup ` +
66
+ `alpine sh -c 'tar czf /backup/${filename} --exclude=.npm/_cacache -C /source . && chmod 644 /backup/${filename}'`,
67
+ { timeout: 60000, stdio: 'pipe' },
68
+ );
69
+
70
+ return NextResponse.json({ success: true, file: filename });
71
+ } catch (e: unknown) {
72
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
73
+ }
74
+ }
75
+
76
+ interface BackupFile {
77
+ name: string;
78
+ size: string;
79
+ sizeBytes: number;
80
+ date: string;
81
+ epoch: number;
82
+ }
83
+
84
+ /**
85
+ * GET /api/agents/[id]/backup
86
+ * List available backups for an agent.
87
+ */
88
+ export async function GET(request: Request,
89
+ { params }: { params: Promise<{ id: string }> },
90
+ ): Promise<NextResponse> {
91
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
92
+ try {
93
+ const { id } = await params;
94
+ if (!id) return NextResponse.json({ error: 'Agent name required' }, { status: 400 });
95
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
96
+
97
+ const prefix = `agent-${id}-`;
98
+
99
+ const raw = execSync(
100
+ `docker run --rm -v ${BACKUP_VOLUME}:/backup alpine sh -c 'stat -c "%s %Y %n" /backup/${prefix}*.tar.gz 2>/dev/null || true'`,
101
+ { timeout: 10000, encoding: 'utf-8' },
102
+ ).trim();
103
+
104
+ const lines = raw.split('\n').filter(Boolean);
105
+ const backups: BackupFile[] = [];
106
+
107
+ for (const line of lines) {
108
+ // Alpine stat -c "%s %Y %n" format: 12345 1711234567 /backup/agent-prometheus-2026-07-11_2151.tar.gz
109
+ const parts = line.split(/\s+/);
110
+ if (parts.length < 3) continue;
111
+ const sizeBytes = parseInt(parts[0], 10) || 0;
112
+ const epoch = parseInt(parts[1], 10) || 0;
113
+ const fullPath = parts.slice(2).join(' ');
114
+ const name = fullPath.replace(/^\/backup\//, '');
115
+ if (!name || !name.startsWith(prefix) || !name.endsWith('.tar.gz')) continue;
116
+ const d = new Date(epoch * 1000);
117
+ const pad = (n: number) => String(n).padStart(2, '0');
118
+ const date = `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
119
+ backups.push({
120
+ name,
121
+ sizeBytes,
122
+ size: fmtSize(sizeBytes),
123
+ date,
124
+ epoch,
125
+ });
126
+ }
127
+
128
+ // Most recent first — sort by the file's real mtime, not by filename order
129
+ // (backup names are not purely chronological: e.g. agent-<id>-prerecreate-<ts>).
130
+ backups.sort((a, b) => b.epoch - a.epoch);
131
+
132
+ return NextResponse.json({ backups });
133
+ } catch (e: unknown) {
134
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
135
+ }
136
+ }
137
+
138
+ /**
139
+ * DELETE /api/agents/[id]/backup?file=agent-xxx-2026-07-11_2115.tar.gz
140
+ * Remove a specific backup file.
141
+ */
142
+ export async function DELETE(request: Request,
143
+ { params }: { params: Promise<{ id: string }> },
144
+ ): Promise<NextResponse> {
145
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
146
+ try {
147
+ const { id } = await params;
148
+ if (!id) return NextResponse.json({ error: 'Agent name required' }, { status: 400 });
149
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
150
+
151
+ const url = new URL(request.url);
152
+ const file = url.searchParams.get('file');
153
+ if (!file) return NextResponse.json({ error: 'file query param required' }, { status: 400 });
154
+
155
+ // Safety: only allow deleting agent-specific files, reject path traversal
156
+ if (!file.startsWith(`agent-${id}-`) || !file.endsWith('.tar.gz') || file.includes('/') || file.includes('..')) {
157
+ return NextResponse.json({ error: 'Invalid backup file name' }, { status: 400 });
158
+ }
159
+
160
+ execSync(
161
+ `docker run --rm -v ${BACKUP_VOLUME}:/backup alpine sh -c 'rm -f /backup/${file}'`,
162
+ { timeout: 10000, stdio: 'pipe' },
163
+ );
164
+
165
+ return NextResponse.json({ success: true });
166
+ } catch (e: unknown) {
167
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
168
+ }
169
+ }
@@ -0,0 +1,69 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { getPairings, approvePairing, revokePairing } from '@/lib/channelManager';
3
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ function getChannel(request: NextRequest): string {
8
+ return request.nextUrl.searchParams.get('channel') || 'telegram';
9
+ }
10
+
11
+ export async function GET(
12
+ request: NextRequest,
13
+ { params }: { params: Promise<{ id: string }> },
14
+ ): Promise<NextResponse> {
15
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
16
+ const { id } = await params;
17
+ const channel = getChannel(request);
18
+ const result = getPairings(id, channel);
19
+ return NextResponse.json(result);
20
+ }
21
+
22
+ export async function POST(
23
+ request: NextRequest,
24
+ { params }: { params: Promise<{ id: string }> },
25
+ ): Promise<NextResponse> {
26
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
27
+ const { id } = await params;
28
+ const channel = getChannel(request);
29
+
30
+ try {
31
+ const body = await request.json();
32
+
33
+ if (!body.code || typeof body.code !== 'string' || !body.code.trim()) {
34
+ return NextResponse.json({ error: 'Pairing code is required' }, { status: 400 });
35
+ }
36
+
37
+ const result = approvePairing(id, body.code.trim(), channel);
38
+
39
+ if (!result.success) {
40
+ return NextResponse.json({ error: result.error }, { status: 404 });
41
+ }
42
+
43
+ return NextResponse.json({ success: true });
44
+ } catch {
45
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
46
+ }
47
+ }
48
+
49
+ export async function DELETE(
50
+ request: NextRequest,
51
+ { params }: { params: Promise<{ id: string }> },
52
+ ): Promise<NextResponse> {
53
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
54
+ const { id } = await params;
55
+ const channel = getChannel(request);
56
+ const senderId = request.nextUrl.searchParams.get('senderId');
57
+
58
+ if (!senderId || !senderId.trim()) {
59
+ return NextResponse.json({ error: 'senderId query param is required' }, { status: 400 });
60
+ }
61
+
62
+ const result = revokePairing(id, senderId, channel);
63
+
64
+ if (!result.success) {
65
+ return NextResponse.json({ error: result.error }, { status: 404 });
66
+ }
67
+
68
+ return NextResponse.json({ success: true });
69
+ }
@@ -0,0 +1,14 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { getChannels } from '@/lib/channelManager';
3
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export async function GET(request: NextRequest,
8
+ { params }: { params: Promise<{ id: string }> },
9
+ ): Promise<NextResponse> {
10
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
11
+ const { id } = await params;
12
+ const result = getChannels(id);
13
+ return NextResponse.json(result);
14
+ }
@@ -0,0 +1,50 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { setTelegramConfig, removeTelegram } from '@/lib/channelManager';
3
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export async function PUT(
8
+ request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> },
10
+ ): Promise<NextResponse> {
11
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
12
+ const { id } = await params;
13
+
14
+ try {
15
+ const body = await request.json();
16
+
17
+ if (!body.botToken || typeof body.botToken !== 'string' || !body.botToken.trim()) {
18
+ return NextResponse.json({ error: 'botToken is required' }, { status: 400 });
19
+ }
20
+
21
+ const result = setTelegramConfig(id, {
22
+ botToken: body.botToken.trim(),
23
+ dmPolicy: body.dmPolicy,
24
+ allowFrom: body.allowFrom,
25
+ });
26
+
27
+ if (!result.success) {
28
+ return NextResponse.json({ error: result.error }, { status: 404 });
29
+ }
30
+
31
+ return NextResponse.json({ success: true });
32
+ } catch {
33
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
34
+ }
35
+ }
36
+
37
+ export async function DELETE(request: NextRequest,
38
+ { params }: { params: Promise<{ id: string }> },
39
+ ): Promise<NextResponse> {
40
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
41
+ const { id } = await params;
42
+
43
+ const result = removeTelegram(id);
44
+
45
+ if (!result.success) {
46
+ return NextResponse.json({ error: result.error }, { status: 404 });
47
+ }
48
+
49
+ return NextResponse.json({ success: true });
50
+ }
@@ -0,0 +1,94 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { execSync } from 'child_process';
3
+ import { resolveAgentedContainer, isValidAgentId, isLifecycleCoolingDown, markLifecycleCooldown } from '@/lib/container';
4
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
5
+
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ const ALLOWED_ACTIONS = new Set(['start', 'stop', 'restart']);
9
+
10
+ /**
11
+ * POST /api/agents/[id]/lifecycle
12
+ * Start, stop, or restart a container by AGENT_ID.
13
+ * Body: { action: "start" | "stop" | "restart" }
14
+ */
15
+ export async function POST(
16
+ request: Request,
17
+ { params }: { params: Promise<{ id: string }> },
18
+ ): Promise<NextResponse> {
19
+ const denied = await requireAuthJWT(request);
20
+ if (denied) return denied as any;
21
+
22
+ try {
23
+ const { id } = await params;
24
+ if (!id) return NextResponse.json({ error: 'Agent ID required' }, { status: 400 });
25
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
26
+
27
+ const containerName = resolveAgentedContainer(id);
28
+ if (!containerName) {
29
+ return NextResponse.json({ error: `No container found with AGENT_ID '${id}'` }, { status: 404 });
30
+ }
31
+
32
+ // Parse and validate action
33
+ let body: { action?: string };
34
+ try {
35
+ body = await request.json();
36
+ } catch {
37
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
38
+ }
39
+
40
+ const action = body.action;
41
+ if (!action || typeof action !== 'string' || !ALLOWED_ACTIONS.has(action)) {
42
+ return NextResponse.json({ error: 'action must be one of: start, stop, restart' }, { status: 400 });
43
+ }
44
+
45
+ // Rate-limit: reject if a lifecycle op was performed < 3s ago
46
+ if (isLifecycleCoolingDown(id)) {
47
+ return NextResponse.json({ error: 'Rate limited — wait a few seconds', status: 'cooling_down' }, { status: 429 });
48
+ }
49
+ markLifecycleCooldown(id);
50
+
51
+ // Check current state to avoid unnecessary operations
52
+ let currentState = 'unknown';
53
+ try {
54
+ currentState = execSync(
55
+ `docker inspect -f '{{.State.Status}}' ${containerName}`,
56
+ { encoding: 'utf-8', timeout: 5000 },
57
+ ).trim();
58
+ } catch {
59
+ return NextResponse.json({ error: `Container ${containerName} not found` }, { status: 404 });
60
+ }
61
+
62
+ // No-op for redundant actions
63
+ if (action === 'start' && currentState === 'running') {
64
+ return NextResponse.json({ status: 'running', containerName, message: 'Already running' });
65
+ }
66
+ if (action === 'stop' && currentState !== 'running') {
67
+ return NextResponse.json({ status: 'stopped', containerName, message: 'Already stopped' });
68
+ }
69
+
70
+ // Execute
71
+ if (action === 'stop') {
72
+ // Gentle stop first, then force-kill if needed
73
+ try {
74
+ execSync(`docker stop -t 30 ${containerName}`, { timeout: 35000 });
75
+ } catch {
76
+ execSync(`docker kill ${containerName}`, { timeout: 10000 });
77
+ }
78
+ } else {
79
+ execSync(`docker ${action} ${containerName}`, { timeout: 30000 });
80
+ }
81
+
82
+ // Read final state
83
+ const newState = execSync(
84
+ `docker inspect -f '{{.State.Status}}' ${containerName}`,
85
+ { encoding: 'utf-8', timeout: 5000 },
86
+ ).trim();
87
+
88
+ return NextResponse.json({ status: newState, containerName });
89
+
90
+ } catch (e: unknown) {
91
+ const msg = (e as Error).message || String(e);
92
+ return NextResponse.json({ error: msg }, { status: 500 });
93
+ }
94
+ }
@@ -0,0 +1,199 @@
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 { requireAuthJWT } from '@/lib/rev4a-auth';
9
+
10
+ export const dynamic = 'force-dynamic';
11
+ export const maxDuration = 180; // docker run (60s) + health check (60s) + config patch (60s)
12
+
13
+ const BACKUP_VOLUME = 'rev4a-backups';
14
+
15
+ /** Timestamp YYYY-MM-DD_HHmmssSSS (ms precision avoids same-second collisions). */
16
+ function ts(): string {
17
+ const d = new Date();
18
+ const pad = (n: number) => String(n).padStart(2, '0');
19
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
20
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}${ms}`;
21
+ }
22
+
23
+ /**
24
+ * Wait for the container gateway to respond on port 3000.
25
+ */
26
+ function waitForContainerReady(name: string, timeoutMs = 60000): boolean {
27
+ const start = Date.now();
28
+ while (Date.now() - start < timeoutMs) {
29
+ try {
30
+ execSync(
31
+ `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'`,
32
+ { timeout: 5000 },
33
+ );
34
+ return true;
35
+ } catch { /* not ready */ }
36
+ execSync('sleep 2', { timeout: 3000 });
37
+ }
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * POST /api/agents/[id]/recreate
43
+ * Stop -> backup -> rm -> run with same params + shared volumes.
44
+ * Workspace data and state are preserved in the persistent volume.
45
+ * Shared volumes (skills, repos) are always mounted.
46
+ * After the container is up, guarantee extraDirs + provider config.
47
+ */
48
+ export async function POST(request: Request,
49
+ { params }: { params: Promise<{ id: string }> },
50
+ ): Promise<NextResponse> {
51
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
52
+ try {
53
+ const { id } = await params;
54
+ if (!id) return NextResponse.json({ error: 'Agent name required' }, { status: 400 });
55
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
56
+
57
+ // Resolve the actual container name via AGENT_ID label
58
+ const containerName = resolveAgentedContainer(id);
59
+ if (!containerName) {
60
+ return NextResponse.json({ error: `No container found with AGENT_ID '${id}'` }, { status: 404 });
61
+ }
62
+
63
+ // Inspect the existing container to capture its parameters
64
+ let inspect: string;
65
+ try {
66
+ inspect = execSync(
67
+ `docker inspect ${containerName}`,
68
+ { timeout: 10000, encoding: 'utf-8' },
69
+ ).trim();
70
+ } catch {
71
+ return NextResponse.json({ error: `Container '${containerName}' not found` }, { status: 404 });
72
+ }
73
+
74
+ const info = JSON.parse(inspect);
75
+ const [container] = info;
76
+
77
+ const image = container.Config?.Image || 'openclaw-agent-base:latest';
78
+ const env: string[] = container.Config?.Env || [];
79
+
80
+ // Filter and reformat env vars (skip system vars, keep AGENT_*, MODEL_*, OPENCLAW_*, TZ)
81
+ const keepPrefixes = ['AGENT_', 'MODEL_', 'OPENCLAW_', 'TZ='];
82
+ const envOpts = env
83
+ .filter((e: string) => keepPrefixes.some((p) => e.startsWith(p)))
84
+ .map((e: string) => `-e ${shellQuote(e)}`)
85
+ .join(' ');
86
+
87
+ // Extract labels (keep AGENT_ID, traefik.*, description, maintainer)
88
+ const labels = container.Config?.Labels || {};
89
+ const keepLabelKeys = ['AGENT_ID', 'traefik.', 'description', 'maintainer'];
90
+ const labelOpts = Object.entries(labels)
91
+ .filter(([k]) => keepLabelKeys.some((p) => k === p || k.startsWith(p)))
92
+ .map(([k, v]) => `-l ${shellQuote(`${k}=${v}`)}`)
93
+ .join(' ');
94
+
95
+ // Determine network (must exist on the container)
96
+ const networks = container.NetworkSettings?.Networks || {};
97
+ const network = Object.keys(networks)[0];
98
+ if (!network) {
99
+ return NextResponse.json({ error: 'Cannot determine network — container may have no networks attached' }, { status: 500 });
100
+ }
101
+
102
+ // Determine if there was a custom port mapping
103
+ const portMappings = (container.HostConfig?.PortBindings || {}) as Record<string, { HostPort: string }[]>;
104
+ const portOpts = Object.entries(portMappings)
105
+ .map(([containerPort, bindings]) => {
106
+ const hostPort = bindings[0]?.HostPort;
107
+ if (!hostPort) return '';
108
+ const proto = containerPort.includes('/udp') ? '/udp' : '';
109
+ return `-p ${hostPort}:${containerPort.replace(/\/.*$/, '')}${proto}`;
110
+ })
111
+ .filter(Boolean)
112
+ .join(' ');
113
+
114
+ const volumeName = `agent-${id}-data`;
115
+
116
+ // Auto-backup the volume BEFORE destroying the container
117
+ const backupFile = `agent-${id}-prerecreate-${ts()}.tar.gz`;
118
+ try {
119
+ execSync(
120
+ `docker run --rm ` +
121
+ `-v ${volumeName}:/source:ro ` +
122
+ `-v ${BACKUP_VOLUME}:/backup ` +
123
+ `alpine sh -c 'tar czf /backup/${backupFile} --exclude=.npm/_cacache -C /source . && chmod 644 /backup/${backupFile}'`,
124
+ { timeout: 60000, stdio: 'pipe' },
125
+ );
126
+ } catch (backupErr) {
127
+ return NextResponse.json(
128
+ { error: `Pre-recreate backup failed, aborting: ${(backupErr as Error).message}` },
129
+ { status: 500 },
130
+ );
131
+ }
132
+
133
+ // Stop & remove the old container. If removal fails (Docker daemon
134
+ // overloaded, container stuck in "removal in progress"), abort — we
135
+ // can't run a new container with a conflicting name.
136
+ try {
137
+ execSync(`docker rm -f ${containerName}`, { timeout: 15000 });
138
+ } catch (rmErr) {
139
+ return NextResponse.json(
140
+ { error: `Failed to remove old container '${containerName}': ${(rmErr as Error).message}. The original container may still be running.` },
141
+ { status: 500 },
142
+ );
143
+ }
144
+
145
+ // Run the new container with all mounts (persistent volume + shared skills + shared repos + rev4a rules)
146
+ const cmd = [
147
+ `docker run -d`,
148
+ `--name "${id}"`,
149
+ `--network "${network}"`,
150
+ `--restart unless-stopped`,
151
+ `--add-host host.docker.internal:host-gateway`,
152
+ `-v ${volumeName}:/root`,
153
+ ...getMountFlags(),
154
+ labelOpts,
155
+ envOpts,
156
+ portOpts,
157
+ `"${image}"`,
158
+ ].filter(Boolean).join(' \\\n ');
159
+
160
+ execSync(cmd, { timeout: 60000, encoding: 'utf-8' });
161
+
162
+ // Wait for the new container to be healthy
163
+ const ready = waitForContainerReady(id);
164
+ if (!ready) {
165
+ return NextResponse.json(
166
+ { success: false, error: 'Container started but gateway did not become healthy within 60s.' },
167
+ { status: 500 },
168
+ );
169
+ }
170
+
171
+ // Guarantee Rev4a runtime config (hooks, shared-skills, provider).
172
+ // Uses config patch (deep-merge) — idempotent, preserves user customizations.
173
+ try {
174
+ applyRuntimeConfig(id);
175
+
176
+ // Also guarantee models.providers.rev4a (host-dependent, not in base image)
177
+ const providerConfig = buildRev4aProviderConfig();
178
+ const providerJson = JSON.stringify({ models: { providers: { rev4a: providerConfig } } });
179
+ execSync(
180
+ `docker exec -i ${id} sh -c 'openclaw config patch --stdin'`,
181
+ { timeout: 15000, stdio: 'pipe', input: providerJson },
182
+ );
183
+
184
+ try {
185
+ execSync(`docker exec ${id} openclaw gateway restart`, { timeout: 15000 });
186
+ } catch { /* non-fatal */ }
187
+ } catch {
188
+ // config patch is best-effort on recreate
189
+ }
190
+
191
+ return NextResponse.json({ success: true, name: id, image, network, backup: backupFile });
192
+ } catch (e: unknown) {
193
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
194
+ }
195
+ }
196
+
197
+ function shellQuote(value: string): string {
198
+ return `'${value.replace(/'/g, `'\\''`)}'`;
199
+ }
@@ -0,0 +1,33 @@
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
+ /**
9
+ * POST /api/agents/[id]/restart
10
+ * Restart a container (docker restart) after verifying it has a matching AGENT_ID label.
11
+ */
12
+ export async function POST(request: Request,
13
+ { params }: { params: Promise<{ id: string }> },
14
+ ): Promise<NextResponse> {
15
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
16
+ try {
17
+ const { id } = await params;
18
+ if (!id) return NextResponse.json({ error: 'Agent name required' }, { status: 400 });
19
+ if (!isValidAgentId(id)) return NextResponse.json({ error: 'Invalid agent id' }, { status: 400 });
20
+
21
+ const containerName = resolveAgentedContainer(id);
22
+ if (!containerName) {
23
+ return NextResponse.json({ error: `No container found with AGENT_ID '${id}'` }, { status: 404 });
24
+ }
25
+
26
+ execSync(`docker restart ${containerName}`, { timeout: 30000 });
27
+
28
+ return NextResponse.json({ success: true });
29
+ } catch (e: unknown) {
30
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
31
+ }
32
+ }
33
+