@flame0510/project-aether 1.3.0 → 1.4.1
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 +1 -0
- package/agent-templates/README.md +42 -22
- package/agent-templates/base-image/Dockerfile +42 -33
- package/agent-templates/base-image/entrypoint.sh +67 -12
- package/app/agents/BrowserAccessSection.tsx +510 -0
- package/app/agents/ChannelManager.tsx +19 -11
- package/app/agents/ImageDownloadBanner.tsx +53 -19
- package/app/agents/ModelSection.tsx +4 -1
- package/app/agents/PageClient.tsx +629 -167
- package/app/agents/UpdateSection.tsx +300 -0
- package/app/agents/create/PageClient.tsx +11 -49
- package/app/agents/create/page.tsx +14 -28
- package/app/api/agents/[id]/backup/route.ts +26 -69
- package/app/api/agents/[id]/channels/pairing/route.ts +3 -3
- package/app/api/agents/[id]/channels/telegram/route.ts +2 -2
- package/app/api/agents/[id]/cold-backup/route.ts +56 -0
- package/app/api/agents/[id]/devices/route.ts +126 -0
- package/app/api/agents/[id]/invite-link/route.ts +53 -0
- package/app/api/agents/[id]/lifecycle/route.ts +3 -0
- package/app/api/agents/[id]/open-control-ui/route.ts +58 -0
- package/app/api/agents/[id]/recreate/route.ts +33 -163
- package/app/api/agents/[id]/restart/route.ts +5 -0
- package/app/api/agents/[id]/restore/route.ts +40 -70
- package/app/api/agents/[id]/route.ts +38 -150
- package/app/api/agents/[id]/update/rollback/route.ts +30 -0
- package/app/api/agents/[id]/update/route.ts +50 -0
- package/app/api/agents/activity-summary/route.ts +67 -0
- package/app/api/agents/create/route.ts +38 -92
- package/app/api/agents/devices-summary/route.ts +37 -0
- package/app/api/agents/download-image/route.ts +16 -9
- package/app/api/agents/image-status/route.ts +31 -111
- package/app/api/agents/route.ts +25 -49
- package/app/api/agents/token/route.ts +33 -10
- package/app/api/assistant/route.ts +2 -2
- package/app/api/gateway/agent/route.ts +14 -0
- package/app/api/gateway/provider/balance/route.ts +5 -2
- package/app/api/gateway/sync.ts +97 -14
- package/app/api/setup/agent-image/route.ts +14 -42
- package/app/api/version/route.ts +2 -1
- package/app/components/DashboardToolbar.tsx +1 -1
- package/app/gateway/PageClient.tsx +27 -32
- package/bin/rev4a.js +43 -41
- package/daemon.js +6 -6
- package/docs/ARCHITECTURE.md +107 -9
- package/docs/FRONTEND-ARCHITECTURE.md +8 -1
- package/docs/REV4A.md +54 -17
- package/docs/dev/API-REFERENCE.md +573 -105
- package/docs/dev/DATABASE.md +96 -0
- package/docs/dev/GATEWAY.md +21 -6
- package/docs/rag/DATA-FRESHNESS.md +6 -4
- package/docs/rag/GLOSSARY.md +12 -3
- package/docs/rag/REV4A-OVERVIEW.md +18 -5
- package/docs/rag/WHAT-I-CAN-ANSWER.md +6 -2
- package/instrumentation.ts +43 -0
- package/lib/agent-busy.ts +21 -0
- package/lib/agent-devices.ts +361 -0
- package/lib/agent-edit-state.ts +108 -0
- package/lib/agent-edit.ts +149 -0
- package/lib/agent-images.ts +375 -0
- package/lib/agent-ports-server.ts +78 -0
- package/lib/agent-ports.ts +91 -0
- package/lib/agent-recreate-state.ts +108 -0
- package/lib/agent-recreate.ts +359 -0
- package/lib/agent-restore-state.ts +107 -0
- package/lib/agent-restore.ts +141 -0
- package/lib/agent-setup.ts +66 -17
- package/lib/agent-update-state.ts +122 -0
- package/lib/agent-update.ts +456 -0
- package/lib/agent-versions.json +14 -0
- package/lib/agent-versions.ts +80 -0
- package/lib/buildAgentImage.ts +88 -290
- package/lib/channelManager.ts +149 -102
- package/lib/cold-backup.ts +354 -0
- package/lib/credentials/delivery.ts +3 -3
- package/lib/db-bootstrap.mjs +76 -0
- package/lib/docker-utils.ts +3 -3
- package/lib/provider-balance.ts +33 -12
- package/package.json +1 -1
package/lib/channelManager.ts
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* - gateway/config-channels.md
|
|
22
22
|
* - concepts/multi-agent.md (bindings)
|
|
23
23
|
*/
|
|
24
|
-
import { execSync
|
|
24
|
+
import { execSync } from 'child_process';
|
|
25
|
+
import { dockerExec, dockerExecShellNoFail } from '@/lib/docker-exec';
|
|
25
26
|
|
|
26
27
|
// ── Types ────────────────────────────────────
|
|
27
28
|
|
|
@@ -134,19 +135,22 @@ export function getChannels(agentId: string): AgentChannels {
|
|
|
134
135
|
|
|
135
136
|
// ── Telegram ─────────────────────────────────
|
|
136
137
|
|
|
137
|
-
export function setTelegramConfig(
|
|
138
|
+
export async function setTelegramConfig(
|
|
138
139
|
agentId: string,
|
|
139
140
|
params: {
|
|
140
141
|
botToken: string;
|
|
141
142
|
dmPolicy?: string;
|
|
142
143
|
allowFrom?: string[];
|
|
143
144
|
},
|
|
144
|
-
): { success: boolean; error?: string } {
|
|
145
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
145
146
|
const container = findContainerById(agentId);
|
|
146
147
|
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
147
148
|
|
|
148
149
|
try {
|
|
149
|
-
|
|
150
|
+
// A failed read is not an empty config: writing `{}` back would replace the agent's
|
|
151
|
+
// whole openclaw.json with the channel block (the historical defect this route had).
|
|
152
|
+
const config = readConfigJson(container);
|
|
153
|
+
if (!config) return { success: false, error: 'Could not read the agent configuration' };
|
|
150
154
|
|
|
151
155
|
// Ensure channels.telegram structure
|
|
152
156
|
if (!config.channels) config.channels = {};
|
|
@@ -161,10 +165,7 @@ export function setTelegramConfig(
|
|
|
161
165
|
// If token changed, wipe old pairing data to avoid orphaned allowFrom entries
|
|
162
166
|
const oldToken = existing.botToken as string | undefined;
|
|
163
167
|
if (oldToken && oldToken !== params.botToken && (existing.dmPolicy as string) === 'pairing') {
|
|
164
|
-
|
|
165
|
-
container,
|
|
166
|
-
`rm -f ${pairingFiles('telegram')} 2>/dev/null`,
|
|
167
|
-
);
|
|
168
|
+
await clearChannelAllowlist(container, 'telegram');
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
// Merge channel config: preserve ALL existing keys (provider, etc.)
|
|
@@ -190,7 +191,7 @@ export function setTelegramConfig(
|
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
export function removeTelegram(agentId: string): { success: boolean; error?: string } {
|
|
194
|
+
export async function removeTelegram(agentId: string): Promise<{ success: boolean; error?: string }> {
|
|
194
195
|
const container = findContainerById(agentId);
|
|
195
196
|
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
196
197
|
|
|
@@ -210,11 +211,8 @@ export function removeTelegram(agentId: string): { success: boolean; error?: str
|
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
container,
|
|
216
|
-
`rm -f ${pairingFiles('telegram')} 2>/dev/null`,
|
|
217
|
-
);
|
|
214
|
+
// The approved senders belong to the disconnected bot: clear the pairing store.
|
|
215
|
+
await clearChannelAllowlist(container, 'telegram');
|
|
218
216
|
|
|
219
217
|
removeBindings(config, agentId, 'telegram');
|
|
220
218
|
writeConfigJson(container, config);
|
|
@@ -291,22 +289,125 @@ export interface PairingState {
|
|
|
291
289
|
approved: ApprovedPairing[];
|
|
292
290
|
}
|
|
293
291
|
|
|
294
|
-
|
|
295
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Script run inside the container to read or edit OpenClaw's channel pairing store.
|
|
294
|
+
*
|
|
295
|
+
* On 2026.9.3 the approved senders live in `~/.openclaw/state/openclaw.sqlite`
|
|
296
|
+
* (`channel_pairing_allow_entries`), and no CLI, RPC or documented export reads or
|
|
297
|
+
* writes them: `openclaw pairing` covers pending requests only, `channels.pairing.*`
|
|
298
|
+
* has no remove, and the writers sit in `dist/pairing-store-<hash>.mjs` with minified
|
|
299
|
+
* export names. The function *names* are preserved, though, so the module is found by
|
|
300
|
+
* glob and its functions by `name` — no hash or minified symbol is hardcoded. The
|
|
301
|
+
* write goes through the same state transaction the CLI uses.
|
|
302
|
+
*
|
|
303
|
+
* `op`, `channel` and `entry` arrive as argv, never interpolated into the source.
|
|
304
|
+
* The sentinel lines are machine-readable: `docker exec` exits 0 even when killed by
|
|
305
|
+
* the timeout, so an empty result cannot be read as success (docs/ARCHITECTURE.md §3.1).
|
|
306
|
+
*/
|
|
307
|
+
const PAIRING_STORE_SCRIPT = `
|
|
308
|
+
(async () => {
|
|
309
|
+
const fs = require('fs');
|
|
310
|
+
const path = require('path');
|
|
311
|
+
const { pathToFileURL } = require('url');
|
|
312
|
+
const [op, channel, entry] = process.argv.slice(1);
|
|
313
|
+
const dist = '/usr/local/lib/node_modules/openclaw/dist';
|
|
314
|
+
let remove, readSync;
|
|
315
|
+
try {
|
|
316
|
+
for (const f of fs.readdirSync(dist).filter((x) => /^pairing-store-.*\\.mjs$/.test(x))) {
|
|
317
|
+
const mod = await import(pathToFileURL(path.join(dist, f)).href);
|
|
318
|
+
for (const v of Object.values(mod)) {
|
|
319
|
+
if (typeof v !== 'function') continue;
|
|
320
|
+
if (v.name === 'removeChannelAllowFromStoreEntry') remove = v;
|
|
321
|
+
if (v.name === 'readChannelAllowFromStoreSync') readSync = v;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (e) { console.log('pair:error ' + e.message); return; }
|
|
325
|
+
if (!remove || !readSync) { console.log('pair:unsupported'); return; }
|
|
326
|
+
// Entries are sender ids as strings today; anything else has a field to read. An
|
|
327
|
+
// unexpected shape is an error, never String(entry) — that would print
|
|
328
|
+
// "[object Object]" and then remove nothing while reporting success.
|
|
329
|
+
const normalize = (list) => (list || []).map((e) => {
|
|
330
|
+
if (typeof e === 'string') return e;
|
|
331
|
+
if (e && typeof e === 'object') { const v = e.id ?? e.senderId ?? e.entry; return typeof v === 'string' ? v : null; }
|
|
332
|
+
return null;
|
|
333
|
+
});
|
|
334
|
+
try {
|
|
335
|
+
if (op === 'read') {
|
|
336
|
+
const entries = normalize(readSync(channel));
|
|
337
|
+
if (entries.some((e) => e === null)) { console.log('pair:error unexpected allowlist entry shape'); return; }
|
|
338
|
+
console.log('pair:list ' + JSON.stringify(entries));
|
|
339
|
+
} else if (op === 'remove') {
|
|
340
|
+
const r = await remove({ channel, entry });
|
|
341
|
+
console.log(r && r.changed ? 'pair:ok' : 'pair:absent');
|
|
342
|
+
} else if (op === 'clear') {
|
|
343
|
+
const entries = normalize(readSync(channel));
|
|
344
|
+
if (entries.some((e) => e === null)) { console.log('pair:error unexpected allowlist entry shape'); return; }
|
|
345
|
+
for (const e of entries) await remove({ channel, entry: e });
|
|
346
|
+
console.log('pair:ok');
|
|
347
|
+
} else {
|
|
348
|
+
console.log('pair:error unknown op');
|
|
349
|
+
}
|
|
350
|
+
} catch (e) { console.log('pair:error ' + e.message); }
|
|
351
|
+
})();
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
interface StoreOpResult {
|
|
355
|
+
status: 'ok' | 'absent' | 'unsupported' | 'error';
|
|
356
|
+
entries?: string[];
|
|
357
|
+
error?: string;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Channel ids reach shell commands and the store API; keep them to OpenClaw's shape. */
|
|
361
|
+
function isSafeChannel(channel: string): boolean {
|
|
362
|
+
return /^[a-z0-9_-]+$/.test(channel);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Run one pairing-store operation inside the container; see PAIRING_STORE_SCRIPT. */
|
|
366
|
+
async function runPairingStore(container: string, op: 'read' | 'remove' | 'clear', channel: string, entry?: string): Promise<StoreOpResult> {
|
|
367
|
+
const argv = ['node', '-e', PAIRING_STORE_SCRIPT, op, channel];
|
|
368
|
+
if (entry !== undefined) argv.push(entry);
|
|
369
|
+
let out: string;
|
|
370
|
+
try {
|
|
371
|
+
// argv reaches docker as an array — the script, the op and the sender id are
|
|
372
|
+
// never shell syntax. Async: the script loads OpenClaw's own modules inside the
|
|
373
|
+
// container and takes a second, which must not freeze Rev4a's event loop while
|
|
374
|
+
// the panel polls it.
|
|
375
|
+
out = (await dockerExec(container, argv, { timeoutMs: 20_000 })).trim();
|
|
376
|
+
} catch (e) {
|
|
377
|
+
return { status: 'error', error: (e as Error).message };
|
|
378
|
+
}
|
|
379
|
+
const listLine = out.split('\n').find((l) => l.startsWith('pair:list '));
|
|
380
|
+
if (listLine) {
|
|
381
|
+
try {
|
|
382
|
+
return { status: 'ok', entries: JSON.parse(listLine.slice('pair:list '.length)) as string[] };
|
|
383
|
+
} catch {
|
|
384
|
+
return { status: 'error', error: 'Could not read the approved senders list' };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (out.includes('pair:ok')) return { status: 'ok' };
|
|
388
|
+
if (out.includes('pair:absent')) return { status: 'absent' };
|
|
389
|
+
if (out.includes('pair:unsupported')) return { status: 'unsupported' };
|
|
390
|
+
const errLine = out.split('\n').find((l) => l.startsWith('pair:error '));
|
|
391
|
+
return { status: 'error', error: errLine ? errLine.slice('pair:error '.length) : (out || 'Pairing store command produced no output') };
|
|
296
392
|
}
|
|
297
393
|
|
|
298
|
-
|
|
299
|
-
|
|
394
|
+
/** Drop every approved sender of a channel — a different bot must not inherit them. */
|
|
395
|
+
async function clearChannelAllowlist(container: string, channel: string): Promise<void> {
|
|
396
|
+
if (!isSafeChannel(channel)) return;
|
|
397
|
+
const result = await runPairingStore(container, 'clear', channel);
|
|
398
|
+
if (result.status === 'error') {
|
|
399
|
+
console.warn(`[channels] could not clear the ${channel} allowlist: ${result.error}`);
|
|
400
|
+
}
|
|
300
401
|
}
|
|
301
402
|
|
|
302
403
|
/** Get pending + approved pairings for a channel */
|
|
303
|
-
export function getPairings(agentId: string, channel: string = 'telegram'): PairingState {
|
|
404
|
+
export async function getPairings(agentId: string, channel: string = 'telegram'): Promise<PairingState> {
|
|
304
405
|
const container = findContainerById(agentId);
|
|
305
406
|
const fallback: PairingState = { pending: [], approved: [] };
|
|
306
|
-
if (!container) return fallback;
|
|
407
|
+
if (!container || !isSafeChannel(channel)) return fallback;
|
|
307
408
|
|
|
308
409
|
// Pending
|
|
309
|
-
const pendingRaw =
|
|
410
|
+
const pendingRaw = await dockerExecShellNoFail(container, `openclaw pairing list --channel ${channel} --json 2>/dev/null`);
|
|
310
411
|
const pending: PendingPairing[] = [];
|
|
311
412
|
if (pendingRaw) {
|
|
312
413
|
try {
|
|
@@ -320,33 +421,25 @@ export function getPairings(agentId: string, channel: string = 'telegram'): Pair
|
|
|
320
421
|
} catch { /* ignore */ }
|
|
321
422
|
}
|
|
322
423
|
|
|
323
|
-
// Approved:
|
|
324
|
-
|
|
424
|
+
// Approved: OpenClaw's pairing store (2026.9.x keeps it in SQLite; the legacy
|
|
425
|
+
// credentials files it replaced are gone, so reading those returned nothing).
|
|
325
426
|
const approved: ApprovedPairing[] = [];
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const entries = parsed?.allowFrom ?? [];
|
|
330
|
-
for (const entry of entries) {
|
|
331
|
-
if (typeof entry === 'string') {
|
|
332
|
-
approved.push({ senderId: entry });
|
|
333
|
-
} else if (entry?.senderId || entry?.id) {
|
|
334
|
-
approved.push({ senderId: entry.senderId ?? entry.id });
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
} catch { /* ignore */ }
|
|
427
|
+
const store = await runPairingStore(container, 'read', channel);
|
|
428
|
+
if (store.status === 'ok') {
|
|
429
|
+
for (const entry of store.entries ?? []) approved.push({ senderId: String(entry) });
|
|
338
430
|
}
|
|
339
431
|
|
|
340
432
|
return { pending, approved };
|
|
341
433
|
}
|
|
342
434
|
|
|
343
435
|
/** Approve a pending pairing code */
|
|
344
|
-
export function approvePairing(agentId: string, code: string, channel: string = 'telegram'): { success: boolean; error?: string } {
|
|
436
|
+
export async function approvePairing(agentId: string, code: string, channel: string = 'telegram'): Promise<{ success: boolean; error?: string }> {
|
|
345
437
|
const container = findContainerById(agentId);
|
|
346
438
|
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
439
|
+
if (!isSafeChannel(channel)) return { success: false, error: 'Invalid channel id' };
|
|
347
440
|
|
|
348
441
|
try {
|
|
349
|
-
const out =
|
|
442
|
+
const out = await dockerExecShellNoFail(container, `openclaw pairing approve ${channel} ${escapeShellArg(code)} 2>&1`);
|
|
350
443
|
if (!out) {
|
|
351
444
|
return { success: false, error: 'Command produced no output — pairing command may not be available' };
|
|
352
445
|
}
|
|
@@ -360,76 +453,30 @@ export function approvePairing(agentId: string, code: string, channel: string =
|
|
|
360
453
|
}
|
|
361
454
|
}
|
|
362
455
|
|
|
363
|
-
/**
|
|
364
|
-
* Script run inside the container to drop one sender from the allowlist file.
|
|
365
|
-
*
|
|
366
|
-
* The sender id arrives as `process.argv[1]`, never interpolated into the source.
|
|
367
|
-
* The previous version built this string by substitution, applying shell-style
|
|
368
|
-
* escaping to a value that lands inside a JavaScript string literal: an id
|
|
369
|
-
* containing an apostrophe closed the literal and the rest ran as code.
|
|
370
|
-
*
|
|
371
|
-
* It prints one of three sentinels so the caller can tell the outcomes apart —
|
|
372
|
-
* `docker exec` exits 0 even when killed by the timeout, so an empty result
|
|
373
|
-
* cannot be read as success (see docs/ARCHITECTURE.md §3.1).
|
|
374
|
-
*/
|
|
375
|
-
const REVOKE_SCRIPT = `
|
|
376
|
-
const fs = require('fs');
|
|
377
|
-
const [p, target] = [process.argv[1], process.argv[2]];
|
|
378
|
-
try {
|
|
379
|
-
if (!fs.existsSync(p)) { console.log('revoke:no-store'); process.exit(0); }
|
|
380
|
-
const d = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
381
|
-
const before = (d.allowFrom || []).length;
|
|
382
|
-
d.allowFrom = (d.allowFrom || []).filter((e) => String(e) !== target);
|
|
383
|
-
if (d.allowFrom.length === before) { console.log('revoke:absent'); process.exit(0); }
|
|
384
|
-
fs.writeFileSync(p, JSON.stringify(d, null, 2), 'utf8');
|
|
385
|
-
console.log('revoke:ok');
|
|
386
|
-
} catch (e) {
|
|
387
|
-
console.log('revoke:error ' + e.message);
|
|
388
|
-
}
|
|
389
|
-
`;
|
|
390
|
-
|
|
391
456
|
/**
|
|
392
457
|
* Revoke an approved sender from the allowlist.
|
|
393
458
|
*
|
|
394
|
-
* There is no CLI for this
|
|
395
|
-
* `help`
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* actually answers `OpenClaw does not know the command "revoke".`, which contains
|
|
401
|
-
* neither, so every revocation reported success, changed nothing, and never
|
|
402
|
-
* reached the fallback.
|
|
403
|
-
*
|
|
404
|
-
* On 2026.9.x this store moved into SQLite and the file is gone. Rather than
|
|
405
|
-
* silently doing nothing again, that case now fails loudly. Revocation against 9.x's SQLite store is not
|
|
406
|
-
* implemented yet.
|
|
459
|
+
* There is no CLI or RPC for this: `openclaw pairing` exposes only `approve`, `list`
|
|
460
|
+
* and `help`, and `channels.pairing.*` has no remove (verified on 2026.9.3). The
|
|
461
|
+
* store's own writer is called instead (`removeChannelAllowFromStoreEntry`, found by
|
|
462
|
+
* name — see PAIRING_STORE_SCRIPT). On a release that no longer exposes it, the
|
|
463
|
+
* failure is explicit: nothing is silently left unchanged (which is what the old
|
|
464
|
+
* `openclaw pairing revoke` probe did).
|
|
407
465
|
*/
|
|
408
|
-
export function revokePairing(agentId: string, senderId: string, channel: string = 'telegram'): { success: boolean; error?: string } {
|
|
466
|
+
export async function revokePairing(agentId: string, senderId: string, channel: string = 'telegram'): Promise<{ success: boolean; error?: string }> {
|
|
409
467
|
const container = findContainerById(agentId);
|
|
410
468
|
if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (out.includes('revoke:absent')) return { success: true }; // already gone — the desired state
|
|
423
|
-
if (out.includes('revoke:no-store')) {
|
|
424
|
-
return {
|
|
425
|
-
success: false,
|
|
426
|
-
error: 'No allowlist file in this container: nobody has been approved on this '
|
|
427
|
-
+ 'channel yet, or the agent runs OpenClaw 9.x, whose pairing store is in '
|
|
428
|
-
+ 'SQLite and cannot be revoked from here yet.',
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
return { success: false, error: out || 'revoke produced no output' };
|
|
432
|
-
} catch (e) {
|
|
433
|
-
return { success: false, error: (e as Error).message };
|
|
469
|
+
if (!isSafeChannel(channel)) return { success: false, error: 'Invalid channel id' };
|
|
470
|
+
|
|
471
|
+
const result = await runPairingStore(container, 'remove', channel, senderId);
|
|
472
|
+
if (result.status === 'ok') return { success: true };
|
|
473
|
+
if (result.status === 'absent') return { success: true }; // already gone — the desired state
|
|
474
|
+
if (result.status === 'unsupported') {
|
|
475
|
+
return {
|
|
476
|
+
success: false,
|
|
477
|
+
error: 'This OpenClaw release does not expose its pairing store to Rev4a. '
|
|
478
|
+
+ 'Revoke the sender with `/allowlist remove` from the chat.',
|
|
479
|
+
};
|
|
434
480
|
}
|
|
481
|
+
return { success: false, error: result.error ?? 'Revoke failed' };
|
|
435
482
|
}
|