@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,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* channelManager.ts
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes channel configuration (Telegram) directly
|
|
5
|
+
* inside agent containers via `docker exec`.
|
|
6
|
+
*
|
|
7
|
+
* ── How it works ──
|
|
8
|
+
*
|
|
9
|
+
* Telegram:
|
|
10
|
+
* - Fully config-file driven. Token is written to
|
|
11
|
+
* channels.telegram.botToken in /root/.openclaw/openclaw.json.
|
|
12
|
+
* - No interactive login needed. Gateway picks it up on next restart.
|
|
13
|
+
*
|
|
14
|
+
* File layout inside agent container:
|
|
15
|
+
* /root/.openclaw/openclaw.json — config
|
|
16
|
+
*
|
|
17
|
+
* ── Reference ──
|
|
18
|
+
*
|
|
19
|
+
* OpenClaw docs:
|
|
20
|
+
* - channels/telegram.md
|
|
21
|
+
* - gateway/config-channels.md
|
|
22
|
+
* - concepts/multi-agent.md (bindings)
|
|
23
|
+
*/
|
|
24
|
+
import { execSync } from 'child_process';
|
|
25
|
+
|
|
26
|
+
// ── Types ────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface TelegramChannel {
|
|
29
|
+
connected: boolean;
|
|
30
|
+
botToken: string; // masked
|
|
31
|
+
dmPolicy: string;
|
|
32
|
+
allowFrom: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AgentChannels {
|
|
36
|
+
agentId: string;
|
|
37
|
+
telegram: TelegramChannel | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Helpers ──────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function containerExec(containerName: string, cmd: string): string {
|
|
43
|
+
return execSync(
|
|
44
|
+
`docker exec ${escapeShellArg(containerName)} sh -c ${escapeShellArg(cmd)}`,
|
|
45
|
+
{ encoding: 'utf-8', timeout: 15000, maxBuffer: 1024 * 1024 * 5 },
|
|
46
|
+
).trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function containerExecNoFail(containerName: string, cmd: string): string {
|
|
50
|
+
try { return containerExec(containerName, cmd); } catch { return ''; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function escapeShellArg(arg: string): string {
|
|
54
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readConfigJson(containerName: string): Record<string, unknown> | null {
|
|
58
|
+
try {
|
|
59
|
+
const raw = containerExecNoFail(containerName, 'cat /root/.openclaw/openclaw.json 2>/dev/null');
|
|
60
|
+
if (!raw) return null;
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeConfigJson(containerName: string, config: Record<string, unknown>): void {
|
|
68
|
+
const json = JSON.stringify(config, null, 2);
|
|
69
|
+
if (!json || json === 'null') {
|
|
70
|
+
throw new Error('Refusing to write empty or invalid config');
|
|
71
|
+
}
|
|
72
|
+
// Pipe JSON via stdin to avoid shell escaping issues
|
|
73
|
+
execSync(
|
|
74
|
+
`docker exec -i ${escapeShellArg(containerName)} sh -c 'cat > /root/.openclaw/openclaw.json.tmp && mv /root/.openclaw/openclaw.json.tmp /root/.openclaw/openclaw.json'`,
|
|
75
|
+
{ input: json, encoding: 'utf-8', timeout: 15000 },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function maskToken(token: string): string {
|
|
80
|
+
if (!token || token.length <= 8) return token ? token.slice(0, 4) + '...' : '';
|
|
81
|
+
return token.slice(0, 4) + '...' + token.slice(-4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findContainerById(agentId: string): string | null {
|
|
85
|
+
// Sanitize: only allow alphanumeric, hyphens, and underscores
|
|
86
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) return null;
|
|
87
|
+
try {
|
|
88
|
+
const out = execSync(
|
|
89
|
+
`docker ps --filter "label=AGENT_ID=${agentId}" --format "{{.Names}}" 2>/dev/null`,
|
|
90
|
+
{ encoding: 'utf-8', timeout: 3000 },
|
|
91
|
+
).trim();
|
|
92
|
+
return out || null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Public API ───────────────────────────────
|
|
99
|
+
|
|
100
|
+
export function getChannels(agentId: string): AgentChannels {
|
|
101
|
+
const container = findContainerById(agentId);
|
|
102
|
+
const result: AgentChannels = { agentId, telegram: null };
|
|
103
|
+
|
|
104
|
+
if (!container) return result;
|
|
105
|
+
|
|
106
|
+
const config = readConfigJson(container);
|
|
107
|
+
if (!config) return result;
|
|
108
|
+
|
|
109
|
+
// ── Telegram ──
|
|
110
|
+
const telegram = (config.channels as Record<string, unknown>)?.telegram as Record<string, unknown> | undefined;
|
|
111
|
+
if (telegram && telegram.enabled !== false) {
|
|
112
|
+
const botToken = (telegram.botToken ?? '') as string;
|
|
113
|
+
const hasToken = typeof botToken === 'string' && botToken.length > 0;
|
|
114
|
+
|
|
115
|
+
if (hasToken) {
|
|
116
|
+
result.telegram = {
|
|
117
|
+
connected: true,
|
|
118
|
+
botToken: maskToken(botToken),
|
|
119
|
+
dmPolicy: (telegram.dmPolicy ?? 'pairing') as string,
|
|
120
|
+
allowFrom: (Array.isArray(telegram.allowFrom) ? telegram.allowFrom : []) as string[],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Telegram ─────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export function setTelegramConfig(
|
|
131
|
+
agentId: string,
|
|
132
|
+
params: {
|
|
133
|
+
botToken: string;
|
|
134
|
+
dmPolicy?: string;
|
|
135
|
+
allowFrom?: string[];
|
|
136
|
+
},
|
|
137
|
+
): { success: boolean; error?: string } {
|
|
138
|
+
const container = findContainerById(agentId);
|
|
139
|
+
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const config = readConfigJson(container) ?? {};
|
|
143
|
+
|
|
144
|
+
// Ensure channels.telegram structure
|
|
145
|
+
if (!config.channels) config.channels = {};
|
|
146
|
+
const channels = config.channels as Record<string, unknown>;
|
|
147
|
+
const existing = (channels.telegram as Record<string, unknown> | undefined) ?? {};
|
|
148
|
+
|
|
149
|
+
// Preserve existing allowFrom if not explicitly overridden
|
|
150
|
+
const existingAllowFrom = params.allowFrom?.length
|
|
151
|
+
? params.allowFrom
|
|
152
|
+
: (existing.allowFrom as string[] | undefined);
|
|
153
|
+
|
|
154
|
+
// If token changed, wipe old pairing data to avoid orphaned allowFrom entries
|
|
155
|
+
const oldToken = existing.botToken as string | undefined;
|
|
156
|
+
if (oldToken && oldToken !== params.botToken && (existing.dmPolicy as string) === 'pairing') {
|
|
157
|
+
containerExecNoFail(
|
|
158
|
+
container,
|
|
159
|
+
`rm -f ${pairingFiles('telegram')} 2>/dev/null`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Merge channel config: preserve ALL existing keys (provider, etc.)
|
|
164
|
+
// and only overwrite the specific fields we're setting.
|
|
165
|
+
channels.telegram = {
|
|
166
|
+
...existing,
|
|
167
|
+
enabled: true,
|
|
168
|
+
botToken: params.botToken,
|
|
169
|
+
dmPolicy: params.dmPolicy ?? existing.dmPolicy ?? 'pairing',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (existingAllowFrom && existingAllowFrom.length > 0) {
|
|
173
|
+
(channels.telegram as Record<string, unknown>).allowFrom = existingAllowFrom;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Ensure a binding exists for this agent → telegram
|
|
177
|
+
ensureBinding(config, agentId, 'telegram', undefined);
|
|
178
|
+
|
|
179
|
+
writeConfigJson(container, config);
|
|
180
|
+
return { success: true };
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return { success: false, error: (e as Error).message };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function removeTelegram(agentId: string): { success: boolean; error?: string } {
|
|
187
|
+
const container = findContainerById(agentId);
|
|
188
|
+
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const config = readConfigJson(container);
|
|
192
|
+
if (!config) return { success: false, error: 'No config found' };
|
|
193
|
+
|
|
194
|
+
const channels = config.channels as Record<string, unknown> | undefined;
|
|
195
|
+
if (channels) {
|
|
196
|
+
// Disable telegram and clear pairing data
|
|
197
|
+
const tg = channels.telegram as Record<string, unknown> | undefined;
|
|
198
|
+
if (tg) {
|
|
199
|
+
tg.enabled = false;
|
|
200
|
+
// Wipe allowFrom and botToken so reconnecting with a different bot starts fresh
|
|
201
|
+
delete tg.botToken;
|
|
202
|
+
delete tg.allowFrom;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Clean up pairing files — they belong to the disconnected bot
|
|
207
|
+
containerExecNoFail(
|
|
208
|
+
container,
|
|
209
|
+
`rm -f ${pairingFiles('telegram')} 2>/dev/null`,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
removeBindings(config, agentId, 'telegram');
|
|
213
|
+
writeConfigJson(container, config);
|
|
214
|
+
return { success: true };
|
|
215
|
+
} catch (e) {
|
|
216
|
+
return { success: false, error: (e as Error).message };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Internal helpers ─────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Ensure a binding exists for channel → "main" agent.
|
|
224
|
+
*
|
|
225
|
+
* The agentId in the binding MUST be "main" (OpenClaw's default agent)
|
|
226
|
+
* so that sessions are created as `agent:main:main`.
|
|
227
|
+
* Using the container name as agentId would create sessions like
|
|
228
|
+
* `agent:NOMEAGENTE:main` which breaks model bindings that target
|
|
229
|
+
* `agent:main:*`.
|
|
230
|
+
*
|
|
231
|
+
* Per multi-agent.md: bindings route (channel, accountId) to agentId.
|
|
232
|
+
* For mono-account, accountId defaults to "default".
|
|
233
|
+
*/
|
|
234
|
+
function ensureBinding(
|
|
235
|
+
config: Record<string, unknown>,
|
|
236
|
+
_agentId: string, // kept for API compatibility, ignored — we always bind to "main"
|
|
237
|
+
channel: string,
|
|
238
|
+
accountId: string | undefined,
|
|
239
|
+
): void {
|
|
240
|
+
const bindingAgentId = 'main'; // always bind to the default agent
|
|
241
|
+
const effectiveAccountId = accountId ?? 'default';
|
|
242
|
+
const bindings = (Array.isArray(config.bindings) ? config.bindings : []) as Record<string, unknown>[];
|
|
243
|
+
|
|
244
|
+
const exists = bindings.some(
|
|
245
|
+
(b) =>
|
|
246
|
+
b.agentId === bindingAgentId &&
|
|
247
|
+
(b.match as Record<string, unknown>)?.channel === channel &&
|
|
248
|
+
((b.match as Record<string, unknown>)?.accountId ?? 'default') === effectiveAccountId,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (!exists) {
|
|
252
|
+
bindings.push({
|
|
253
|
+
agentId: bindingAgentId,
|
|
254
|
+
match: {
|
|
255
|
+
channel,
|
|
256
|
+
accountId: effectiveAccountId,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
config.bindings = bindings;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function removeBindings(config: Record<string, unknown>, _agentId: string, channel: string): void {
|
|
264
|
+
const bindingAgentId = 'main';
|
|
265
|
+
const bindings = (Array.isArray(config.bindings) ? config.bindings : []) as Record<string, unknown>[];
|
|
266
|
+
config.bindings = bindings.filter(
|
|
267
|
+
(b) => !(b.agentId === bindingAgentId && (b.match as Record<string, unknown>)?.channel === channel),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Pairing ──────────────────────────────────
|
|
272
|
+
|
|
273
|
+
export interface PendingPairing {
|
|
274
|
+
code: string;
|
|
275
|
+
senderId: string | null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface ApprovedPairing {
|
|
279
|
+
senderId: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface PairingState {
|
|
283
|
+
pending: PendingPairing[];
|
|
284
|
+
approved: ApprovedPairing[];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function pairingAllowFile(channel: string): string {
|
|
288
|
+
return `/root/.openclaw/credentials/${channel}-default-allowFrom.json`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function pairingFiles(channel: string): string {
|
|
292
|
+
return `/root/.openclaw/credentials/${channel}-pairing.json /root/.openclaw/credentials/${channel}-default-allowFrom.json`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Get pending + approved pairings for a channel */
|
|
296
|
+
export function getPairings(agentId: string, channel: string = 'telegram'): PairingState {
|
|
297
|
+
const container = findContainerById(agentId);
|
|
298
|
+
const fallback: PairingState = { pending: [], approved: [] };
|
|
299
|
+
if (!container) return fallback;
|
|
300
|
+
|
|
301
|
+
// Pending
|
|
302
|
+
const pendingRaw = containerExecNoFail(container, `openclaw pairing list --channel ${channel} --json 2>/dev/null`);
|
|
303
|
+
const pending: PendingPairing[] = [];
|
|
304
|
+
if (pendingRaw) {
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(pendingRaw);
|
|
307
|
+
for (const req of parsed.requests ?? []) {
|
|
308
|
+
pending.push({
|
|
309
|
+
code: req.code ?? req.id ?? '',
|
|
310
|
+
senderId: req.senderId ?? null,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
} catch { /* ignore */ }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Approved: read from OpenClaw credentials file
|
|
317
|
+
const allowRaw = containerExecNoFail(container, `cat ${pairingAllowFile(channel)} 2>/dev/null`);
|
|
318
|
+
const approved: ApprovedPairing[] = [];
|
|
319
|
+
if (allowRaw) {
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(allowRaw);
|
|
322
|
+
const entries = parsed?.allowFrom ?? [];
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
if (typeof entry === 'string') {
|
|
325
|
+
approved.push({ senderId: entry });
|
|
326
|
+
} else if (entry?.senderId || entry?.id) {
|
|
327
|
+
approved.push({ senderId: entry.senderId ?? entry.id });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch { /* ignore */ }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { pending, approved };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Approve a pending pairing code */
|
|
337
|
+
export function approvePairing(agentId: string, code: string, channel: string = 'telegram'): { success: boolean; error?: string } {
|
|
338
|
+
const container = findContainerById(agentId);
|
|
339
|
+
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const out = containerExecNoFail(container, `openclaw pairing approve ${channel} ${escapeShellArg(code)} 2>&1`);
|
|
343
|
+
if (!out) {
|
|
344
|
+
return { success: false, error: 'Command produced no output — pairing command may not be available' };
|
|
345
|
+
}
|
|
346
|
+
if (out.includes('error')) {
|
|
347
|
+
return { success: false, error: out };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { success: true };
|
|
351
|
+
} catch (e) {
|
|
352
|
+
return { success: false, error: (e as Error).message };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Revoke an approved sender from the allowlist */
|
|
357
|
+
export function revokePairing(agentId: string, senderId: string, channel: string = 'telegram'): { success: boolean; error?: string } {
|
|
358
|
+
const container = findContainerById(agentId);
|
|
359
|
+
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Try native revoke command first, fall back to direct file manipulation
|
|
363
|
+
const nativeOut = containerExecNoFail(
|
|
364
|
+
container,
|
|
365
|
+
`openclaw pairing revoke ${channel} ${escapeShellArg(senderId)} 2>&1`,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
if (nativeOut && !nativeOut.includes('Unknown command') && !nativeOut.includes('not found')) {
|
|
369
|
+
return { success: true };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Fallback: remove from credentials file directly
|
|
373
|
+
const filePath = pairingAllowFile(channel);
|
|
374
|
+
const fallbackOut = containerExecNoFail(
|
|
375
|
+
container,
|
|
376
|
+
`node -e "const p='${filePath}';try{const d=JSON.parse(require('fs').readFileSync(p,'utf8'));d.allowFrom=(d.allowFrom||[]).filter(e=>e!=='${senderId.replace(/'/g, "'\\''")}');require('fs').writeFileSync(p,JSON.stringify(d,null,2),'utf8')}catch(e){console.error('revoke fallback error:',e.message)}"`,
|
|
377
|
+
);
|
|
378
|
+
if (fallbackOut && fallbackOut.includes('revoke fallback error:')) {
|
|
379
|
+
return { success: false, error: fallbackOut };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { success: true };
|
|
383
|
+
} catch (e) {
|
|
384
|
+
return { success: false, error: (e as Error).message };
|
|
385
|
+
}
|
|
386
|
+
}
|
package/lib/container.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const AGENT_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate an agent id/name against the same rules used at creation.
|
|
7
|
+
* Guards against shell injection in routes that interpolate the id into
|
|
8
|
+
* docker commands. Returns true if the id is safe to use.
|
|
9
|
+
*/
|
|
10
|
+
export function isValidAgentId(id: string): boolean {
|
|
11
|
+
return typeof id === 'string' && id.length >= 1 && id.length <= 64 && AGENT_ID_REGEX.test(id);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Escape an agent id for use in `docker ps --filter` label values.
|
|
16
|
+
* Only safe characters pass through; returns empty string on unsafe input.
|
|
17
|
+
*/
|
|
18
|
+
export function safeAgentId(id: string): string {
|
|
19
|
+
return AGENT_ID_REGEX.test(id) ? id : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the container name from an AGENT_ID label.
|
|
24
|
+
* @returns The container name, or null if no container is found.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveAgentedContainer(agentId: string): string | null {
|
|
27
|
+
const safe = safeAgentId(agentId);
|
|
28
|
+
if (!safe) return null;
|
|
29
|
+
try {
|
|
30
|
+
const found = execSync(
|
|
31
|
+
`docker ps -a --filter "label=AGENT_ID=${safe}" --format '{{.Names}}'`,
|
|
32
|
+
{ encoding: 'utf-8', timeout: 5000 },
|
|
33
|
+
).trim();
|
|
34
|
+
return found || null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Operations that mutate containers may trigger their own polling.
|
|
42
|
+
* Keep a lightweight in-memory debounce so rapid toggle clicks don't stack.
|
|
43
|
+
*/
|
|
44
|
+
const lifecycleCooldown = new Map<string, number>();
|
|
45
|
+
const LIFECYCLE_COOLDOWN_MS = 3_000;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns true if the agent is currently in cooldown (lifecycle operation in progress).
|
|
49
|
+
*/
|
|
50
|
+
export function isLifecycleCoolingDown(agentId: string): boolean {
|
|
51
|
+
const last = lifecycleCooldown.get(agentId);
|
|
52
|
+
return last != null && Date.now() - last < LIFECYCLE_COOLDOWN_MS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mark an agent as having a lifecycle operation in progress.
|
|
57
|
+
*/
|
|
58
|
+
export function markLifecycleCooldown(agentId: string): void {
|
|
59
|
+
lifecycleCooldown.set(agentId, Date.now());
|
|
60
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { DB_PATH } from '@/lib/db';
|
|
5
|
+
|
|
6
|
+
// credentials.db lives alongside events.db in the same data directory
|
|
7
|
+
export const CREDENTIALS_DB_PATH =
|
|
8
|
+
process.env.REV4A_CREDENTIALS_DB ??
|
|
9
|
+
path.join(path.dirname(DB_PATH), 'credentials.db');
|
|
10
|
+
|
|
11
|
+
function bootstrap(): void {
|
|
12
|
+
fs.mkdirSync(path.dirname(CREDENTIALS_DB_PATH), { recursive: true });
|
|
13
|
+
const db = new Database(CREDENTIALS_DB_PATH);
|
|
14
|
+
db.pragma('journal_mode = WAL');
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS credential_profiles (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
provider_id TEXT NOT NULL,
|
|
19
|
+
label TEXT NOT NULL,
|
|
20
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
21
|
+
secret_masked TEXT,
|
|
22
|
+
created_at INTEGER NOT NULL,
|
|
23
|
+
updated_at INTEGER NOT NULL,
|
|
24
|
+
last_synced_at INTEGER
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS credential_secrets (
|
|
28
|
+
profile_id TEXT PRIMARY KEY,
|
|
29
|
+
payload TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS credential_audit_log (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
ts INTEGER NOT NULL,
|
|
35
|
+
actor TEXT NOT NULL,
|
|
36
|
+
action TEXT NOT NULL,
|
|
37
|
+
profile_id TEXT,
|
|
38
|
+
result TEXT
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_cred_profiles_provider ON credential_profiles(provider_id);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_cred_audit_ts ON credential_audit_log(ts);
|
|
43
|
+
`);
|
|
44
|
+
db.close();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Track whether the full bootstrap (including migrations) has run
|
|
48
|
+
let ready = false;
|
|
49
|
+
|
|
50
|
+
function migrateV1(): void {
|
|
51
|
+
// v0 → v1: rename encrypted_payload → payload, drop key_version
|
|
52
|
+
// This is safe because credentials.db was never shipped with real data
|
|
53
|
+
const db = new Database(CREDENTIALS_DB_PATH);
|
|
54
|
+
try {
|
|
55
|
+
// Check if the old column exists
|
|
56
|
+
const cols = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='credential_secrets'").get() as { sql: string } | null;
|
|
57
|
+
if (cols && cols.sql.includes('encrypted_payload')) {
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE TABLE credential_secrets_v1 (
|
|
60
|
+
profile_id TEXT PRIMARY KEY,
|
|
61
|
+
payload TEXT NOT NULL
|
|
62
|
+
);
|
|
63
|
+
INSERT INTO credential_secrets_v1 (profile_id, payload)
|
|
64
|
+
SELECT profile_id, encrypted_payload FROM credential_secrets;
|
|
65
|
+
DROP TABLE credential_secrets;
|
|
66
|
+
ALTER TABLE credential_secrets_v1 RENAME TO credential_secrets;
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let migrationDone = false;
|
|
75
|
+
|
|
76
|
+
export function openCredentialsDb(readonly = true): Database.Database {
|
|
77
|
+
if (!ready) {
|
|
78
|
+
bootstrap();
|
|
79
|
+
ready = true;
|
|
80
|
+
}
|
|
81
|
+
if (!migrationDone && !readonly) {
|
|
82
|
+
migrateV1();
|
|
83
|
+
migrationDone = true;
|
|
84
|
+
}
|
|
85
|
+
return new Database(CREDENTIALS_DB_PATH, { readonly });
|
|
86
|
+
}
|