@flame0510/project-aether 1.1.15 → 1.3.0

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 (60) hide show
  1. package/README.md +2 -1
  2. package/app/agents/ModelSection.tsx +313 -0
  3. package/app/agents/PageClient.tsx +83 -4
  4. package/app/agents/create/page.tsx +8 -21
  5. package/app/api/agents/[id]/model/route.ts +113 -0
  6. package/app/api/agents/[id]/recreate/route.ts +10 -34
  7. package/app/api/agents/[id]/route.ts +10 -29
  8. package/app/api/agents/create/route.ts +59 -57
  9. package/app/api/agents/models-summary/route.ts +163 -0
  10. package/app/api/assistant/route.ts +36 -15
  11. package/app/api/credentials/[id]/sync/route.ts +3 -3
  12. package/app/api/credentials/detect/route.ts +126 -176
  13. package/app/api/credentials/route.ts +3 -0
  14. package/app/api/gateway/agent/route.ts +23 -6
  15. package/app/api/gateway/provider/keys.ts +13 -1
  16. package/app/api/gateway/provider/route.ts +43 -12
  17. package/app/api/gateway/sync.ts +248 -72
  18. package/app/api/models/route.ts +28 -34
  19. package/app/api/provider/auth.ts +65 -0
  20. package/app/api/provider/upstream.ts +9 -2
  21. package/app/api/provider/v1/chat/completions/route.ts +22 -16
  22. package/app/api/provider/v1/models/route.ts +26 -133
  23. package/app/components/PulseChat.tsx +25 -39
  24. package/app/components/Skeleton.tsx +132 -0
  25. package/app/components/ui/RemoveButton.tsx +46 -0
  26. package/app/components/ui/Select.tsx +3 -2
  27. package/app/components/ui/index.ts +1 -0
  28. package/app/credentials/PageClient.tsx +461 -140
  29. package/app/credentials/loading.tsx +19 -5
  30. package/app/gateway/PageClient.tsx +257 -673
  31. package/app/globals.css +8 -0
  32. package/app/lib/models-context.tsx +43 -7
  33. package/app/wizard/useWizard.ts +6 -1
  34. package/bin/rev4a.js +73 -9
  35. package/docs/ARCHITECTURE.md +92 -33
  36. package/docs/FRONTEND-ARCHITECTURE.md +36 -6
  37. package/docs/REV4A.md +62 -30
  38. package/docs/dev/API-REFERENCE.md +490 -227
  39. package/docs/dev/DATABASE.md +8 -3
  40. package/docs/dev/GATEWAY.md +236 -92
  41. package/docs/dev/PROVIDERS.md +44 -44
  42. package/docs/rag/DATA-FRESHNESS.md +57 -28
  43. package/docs/rag/GLOSSARY.md +20 -18
  44. package/docs/rag/REV4A-OVERVIEW.md +28 -32
  45. package/docs/rag/WHAT-I-CAN-ANSWER.md +10 -12
  46. package/instrumentation.ts +9 -1
  47. package/lib/agent-readiness.ts +110 -0
  48. package/lib/channelManager.ts +64 -22
  49. package/lib/container-file.ts +27 -0
  50. package/lib/credentials/delivery.ts +212 -119
  51. package/lib/credentials/detect.ts +229 -97
  52. package/lib/credentials/providers.ts +38 -7
  53. package/lib/credentials/vault.ts +78 -13
  54. package/lib/docker-exec.ts +50 -14
  55. package/lib/model-catalogue.ts +140 -27
  56. package/lib/rev4a-paths.ts +0 -21
  57. package/model-pricing.json +118 -110
  58. package/models.config.json +27 -12
  59. package/package.json +1 -1
  60. package/app/api/gateway/route.ts +0 -191
@@ -21,7 +21,7 @@
21
21
  * - gateway/config-channels.md
22
22
  * - concepts/multi-agent.md (bindings)
23
23
  */
24
- import { execSync } from 'child_process';
24
+ import { execSync, execFileSync } from 'child_process';
25
25
 
26
26
  // ── Types ────────────────────────────────────
27
27
 
@@ -232,7 +232,7 @@ export function removeTelegram(agentId: string): { success: boolean; error?: str
232
232
  * The agentId in the binding MUST be "main" (OpenClaw's default agent)
233
233
  * so that sessions are created as `agent:main:main`.
234
234
  * Using the container name as agentId would create sessions like
235
- * `agent:NOMEAGENTE:main` which breaks model bindings that target
235
+ * `agent:<AGENT_NAME>:main` which breaks model bindings that target
236
236
  * `agent:main:*`.
237
237
  *
238
238
  * Per multi-agent.md: bindings route (channel, accountId) to agentId.
@@ -360,33 +360,75 @@ export function approvePairing(agentId: string, code: string, channel: string =
360
360
  }
361
361
  }
362
362
 
363
- /** Revoke an approved sender from the allowlist */
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
+ /**
392
+ * Revoke an approved sender from the allowlist.
393
+ *
394
+ * There is no CLI for this. `openclaw pairing` exposes only `approve`, `list` and
395
+ * `help` — verified on both 2026.7.1-2 and 2026.9.3 — so the allowlist store has
396
+ * to be edited directly.
397
+ *
398
+ * The previous implementation tried `openclaw pairing revoke` first and treated
399
+ * any output that lacked "Unknown command" or "not found" as success. OpenClaw
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.
407
+ */
364
408
  export function revokePairing(agentId: string, senderId: string, channel: string = 'telegram'): { success: boolean; error?: string } {
365
409
  const container = findContainerById(agentId);
366
410
  if (!container) return { success: false, error: `Agent container not found: ${agentId}` };
367
411
 
368
412
  try {
369
- // Try native revoke command first, fall back to direct file manipulation
370
- const nativeOut = containerExecNoFail(
371
- container,
372
- `openclaw pairing revoke ${channel} ${escapeShellArg(senderId)} 2>&1`,
373
- );
374
-
375
- if (nativeOut && !nativeOut.includes('Unknown command') && !nativeOut.includes('not found')) {
376
- return { success: true };
377
- }
413
+ // execFileSync, not a shell: argv is passed through untouched, so nothing in
414
+ // `senderId` can be read as syntax.
415
+ const out = execFileSync(
416
+ 'docker',
417
+ ['exec', container, 'node', '-e', REVOKE_SCRIPT, pairingAllowFile(channel), senderId],
418
+ { encoding: 'utf-8', timeout: 15000, maxBuffer: 1024 * 1024 },
419
+ ).trim();
378
420
 
379
- // Fallback: remove from credentials file directly
380
- const filePath = pairingAllowFile(channel);
381
- const fallbackOut = containerExecNoFail(
382
- container,
383
- `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)}"`,
384
- );
385
- if (fallbackOut && fallbackOut.includes('revoke fallback error:')) {
386
- return { success: false, error: fallbackOut };
421
+ if (out.includes('revoke:ok')) return { success: true };
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
+ };
387
430
  }
388
-
389
- return { success: true };
431
+ return { success: false, error: out || 'revoke produced no output' };
390
432
  } catch (e) {
391
433
  return { success: false, error: (e as Error).message };
392
434
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Reading one file out of an agent container, running or not.
3
+ *
4
+ * `docker exec` cannot reach a stopped or paused container; `docker cp` can, and
5
+ * streams the file as a tar archive. Used by the config sync, which also writes
6
+ * back through `docker cp`, and by the model summary behind the agent cards.
7
+ */
8
+
9
+ /** The single regular file in a `docker cp … -` tar stream: its bytes and mode. */
10
+ export function singleFileFromTar(tar: Buffer): { body: Buffer; mode: number } {
11
+ if (tar.length < 512) throw new Error('docker cp returned an empty archive');
12
+ const header = tar.subarray(0, 512);
13
+ const field = (start: number, len: number) => {
14
+ const text = header.subarray(start, start + len).toString('ascii');
15
+ const nul = text.indexOf('\0');
16
+ return (nul === -1 ? text : text.slice(0, nul)).trim();
17
+ };
18
+ const type = field(156, 1);
19
+ // '0' (or NUL on old writers) is a regular file. Anything else — a pax header,
20
+ // a directory — is not the shape expected, and guessing past it could mean
21
+ // writing a config that was never read.
22
+ if (type !== '0' && type !== '') throw new Error(`docker cp returned a non-file entry (type ${type})`);
23
+ const size = parseInt(field(124, 12), 8);
24
+ if (!Number.isFinite(size) || tar.length < 512 + size) throw new Error('docker cp returned a truncated archive');
25
+ const mode = parseInt(field(100, 8), 8);
26
+ return { body: tar.subarray(512, 512 + size), mode: Number.isFinite(mode) ? mode & 0o777 : 0o600 };
27
+ }