@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.
- package/README.md +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- 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
|
+
}
|