aquaman-proxy 0.12.1 → 0.13.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.
Files changed (50) hide show
  1. package/README.md +13 -13
  2. package/dist/cli/index.js +378 -87
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/audit/logger.js +1 -1
  5. package/dist/core/audit/logger.js.map +1 -1
  6. package/dist/core/credentials/backends/onepassword.js +1 -1
  7. package/dist/core/credentials/backends/onepassword.js.map +1 -1
  8. package/dist/core/credentials/backends/systemd-creds.js +1 -1
  9. package/dist/core/credentials/backends/systemd-creds.js.map +1 -1
  10. package/dist/core/credentials/caching-store.d.ts +43 -0
  11. package/dist/core/credentials/caching-store.d.ts.map +1 -0
  12. package/dist/core/credentials/caching-store.js +77 -0
  13. package/dist/core/credentials/caching-store.js.map +1 -0
  14. package/dist/core/credentials/index.d.ts +1 -0
  15. package/dist/core/credentials/index.d.ts.map +1 -1
  16. package/dist/core/credentials/index.js +1 -0
  17. package/dist/core/credentials/index.js.map +1 -1
  18. package/dist/core/index.d.ts +2 -2
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/index.js +2 -2
  21. package/dist/core/index.js.map +1 -1
  22. package/dist/core/types.d.ts +34 -0
  23. package/dist/core/types.d.ts.map +1 -1
  24. package/dist/core/utils/config.d.ts +25 -0
  25. package/dist/core/utils/config.d.ts.map +1 -1
  26. package/dist/core/utils/config.js +82 -2
  27. package/dist/core/utils/config.js.map +1 -1
  28. package/dist/core/utils/index.d.ts +1 -1
  29. package/dist/core/utils/index.d.ts.map +1 -1
  30. package/dist/core/utils/index.js +1 -1
  31. package/dist/core/utils/index.js.map +1 -1
  32. package/dist/daemon.d.ts +26 -0
  33. package/dist/daemon.d.ts.map +1 -1
  34. package/dist/daemon.js +104 -6
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/hermes/config-writer.d.ts +71 -0
  37. package/dist/hermes/config-writer.d.ts.map +1 -0
  38. package/dist/hermes/config-writer.js +137 -0
  39. package/dist/hermes/config-writer.js.map +1 -0
  40. package/dist/hermes/integration.d.ts +53 -0
  41. package/dist/hermes/integration.d.ts.map +1 -0
  42. package/dist/hermes/integration.js +85 -0
  43. package/dist/hermes/integration.js.map +1 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +3 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/openclaw/integration.d.ts.map +1 -1
  49. package/dist/openclaw/integration.js.map +1 -1
  50. package/package.json +4 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # aquaman-proxy
2
2
 
3
- The vault + daemon + audit core of [aquaman](https://github.com/tech4242/aquaman). API key protection for AI agents — credentials stay in your vault, never in the agent's memory.
3
+ The vault + daemon + audit core of [aquaman](https://github.com/tech4242/aquaman). API key protection for AI agents. Bring your own vault. Credentials stay where you already keep them, never in the agent's memory.
4
4
 
5
5
  This is the **always-on piece**: every other aquaman package (`aquaman-plugin` for OpenClaw, `aquaman-coder` for AI coding agents) talks to it. If you only install one aquaman package, install this one.
6
6
 
@@ -8,7 +8,7 @@ This is the **always-on piece**: every other aquaman package (`aquaman-plugin` f
8
8
  Agent / OpenClaw / Coding Agent Aquaman Proxy
9
9
  ┌──────────────────────┐ ┌──────────────────────┐
10
10
  │ │ │ │
11
- │ ANTHROPIC_BASE_URL │═══════ UDS ════════>│ Keychain / 1Pass / │
11
+ │ ANTHROPIC_BASE_URL │════ UDS / HTTP ════>│ Keychain / 1Pass / │
12
12
  │ = aquaman.local │ │ Vault / Encrypted │
13
13
  │ │<══════════════════ │ │
14
14
  │ fetch() interceptor │═══ broker:resolve ═>│ + Policy enforced │
@@ -18,7 +18,7 @@ Agent / OpenClaw / Coding Agent Aquaman Proxy
18
18
  │ No credentials. │ proxy.sock │ │
19
19
  │ No open ports. │ (chmod 0o600) │ │
20
20
  │ Nothing to steal. │ │ │
21
- └──────────────────────┘ └───┬──────────┬───────┘
21
+ └──────────────────────┘ └──┬──────────┬────────┘
22
22
  │ │
23
23
  │ ▼
24
24
  │ ~/.aquaman/audit/
@@ -57,16 +57,16 @@ aquaman daemon & # start the proxy on ~/.aquaman/proxy.sock
57
57
 
58
58
  Four layers of protection:
59
59
 
60
- - **Process isolation** — credentials in a separate address space, connected via UDS (`chmod 0o600`)
61
- - **Service allowlisting** — `proxiedServices` controls which APIs the agent can reach
62
- - **Request policies** — method + path rules per service, checked *before* credential injection ([details in the root README](https://github.com/tech4242/aquaman#request-policies))
63
- - **Audit trail** — SHA-256 hash-chained logs of every credential use
60
+ - **Process isolation**: credentials in a separate address space, connected via UDS (`chmod 0o600`)
61
+ - **Service allowlisting**: `proxiedServices` controls which APIs the agent can reach
62
+ - **Request policies**: method + path rules per service, checked *before* credential injection ([details in the root README](https://github.com/tech4242/aquaman#request-policies))
63
+ - **Audit trail**: SHA-256 hash-chained logs of every credential use
64
64
 
65
- 7 credential backends: Keychain, 1Password, HashiCorp Vault, Bitwarden, KeePassXC, systemd-creds, encrypted-file.
65
+ **Bring your own vault.** Aquaman has no house vault. It injects credentials from the secret store you already run: Keychain, 1Password, HashiCorp Vault, Bitwarden, KeePassXC, systemd-creds, or encrypted-file. No new store to adopt, no migration.
66
66
 
67
67
  ## Broker endpoint (v0.12.0+)
68
68
 
69
- `POST /broker/resolve` over the UDS — used by `aquaman-coder` to materialize credentials per tool call. Body:
69
+ `POST /broker/resolve` over the UDS - used by `aquaman-coder` to materialize credentials per tool call. Body:
70
70
 
71
71
  ```json
72
72
  {"service":"anthropic","key":"api_key","ttl_seconds":60}
@@ -76,10 +76,10 @@ Response: `{"value":"...","expires_at":"2026-05-20T12:34:56Z"}`. Validates servi
76
76
 
77
77
  ## Documentation
78
78
 
79
- - **[Root README](https://github.com/tech4242/aquaman#readme)** — value prop, three-path Quick Start, security model
80
- - **[`docs/PACKAGES.md`](../../docs/PACKAGES.md)** — package boundary policy
81
- - **[`docs/compliance/`](../../docs/compliance/)** — MITRE ATLAS + NIST SP 800-53 mappings
82
- - **[`CLAUDE.md`](../../CLAUDE.md)** — architecture notes
79
+ - **[Root README](https://github.com/tech4242/aquaman#readme)**: value prop, three-path Quick Start, security model
80
+ - **[`docs/PACKAGES.md`](../../docs/PACKAGES.md)**: package boundary policy
81
+ - **[`docs/compliance/`](../../docs/compliance/)**: MITRE ATLAS + NIST SP 800-53 mappings
82
+ - **[`CLAUDE.md`](../../CLAUDE.md)**: architecture notes
83
83
 
84
84
  ## License
85
85
 
package/dist/cli/index.js CHANGED
@@ -12,11 +12,13 @@ import { Command, Help } from 'commander';
12
12
  import * as path from 'node:path';
13
13
  import * as fs from 'node:fs';
14
14
  import * as http from 'node:http';
15
- import { loadConfig, getConfigDir, ensureConfigDir, getDefaultConfig, expandPath, createAuditLogger, createCredentialStore, saveConfig } from '../core/index.js';
15
+ import { loadConfig, getConfigDir, ensureConfigDir, getDefaultConfig, expandPath, createAuditLogger, createCredentialStore, saveConfig, generateLoopbackToken, DEFAULT_LOOPBACK_PORT, wrapWithCache, resolveCacheTtl } from '../core/index.js';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { createCredentialProxy } from '../daemon.js';
18
18
  import { createServiceRegistry, ServiceRegistry } from '../service-registry.js';
19
19
  import { createOpenClawIntegration, authProfilesAreSqliteOnly } from '../openclaw/integration.js';
20
+ import { createHermesIntegration, detectHermes } from '../hermes/integration.js';
21
+ import { managedScopeShadowedKeys, HERMES_MANAGED_ENV_PATH } from '../hermes/config-writer.js';
20
22
  import { loadPolicyFromConfig, validatePolicyConfig, getDefaultPolicyPresets, matchPolicy } from '../request-policy.js';
21
23
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
22
24
  // Read version from package.json (single source of truth)
@@ -37,6 +39,81 @@ function validateCredName(label, value) {
37
39
  process.exit(1);
38
40
  }
39
41
  }
42
+ /**
43
+ * Soft hint for 1Password users running in biometric/desktop-app mode: every
44
+ * `op` spawn prompts, which makes unattended agents unusable. The daemon cache
45
+ * (v0.13.1+) bounds it to one prompt per TTL per credential; a service account
46
+ * removes prompts entirely. Printed by `aquaman doctor` and deep doctors.
47
+ */
48
+ function printOnePasswordModeHint(config, indent = ' ') {
49
+ if (config.credentials.backend !== '1password')
50
+ return;
51
+ if (process.env['OP_SERVICE_ACCOUNT_TOKEN'] || process.env['OP_CONNECT_HOST'])
52
+ return;
53
+ const vault = config.credentials.onePasswordVault || 'aquaman';
54
+ const ttl = resolveCacheTtl(config);
55
+ const cacheNote = ttl > 0
56
+ ? `the daemon cache bounds this to one per ${Math.round(ttl / 60)} min per credential`
57
+ : 'and the credential cache is disabled (cacheTtlSeconds: 0)';
58
+ console.log(`${indent}• 1Password is in biometric/desktop-app mode — each vault read prompts (${cacheNote}).`);
59
+ console.log(`${indent} For zero prompts, create a service account scoped to the "${vault}" vault (create the`);
60
+ console.log(`${indent} vault + credentials as a human first, or grant write_items) and start the daemon with`);
61
+ console.log(`${indent} OP_SERVICE_ACCOUNT_TOKEN. If OP_CONNECT_HOST/OP_CONNECT_TOKEN are set they take precedence.`);
62
+ }
63
+ /**
64
+ * Map config.credentials onto CredentialStoreOptions — the single source for
65
+ * every createCredentialStore() call in this CLI. Env fallbacks mirror
66
+ * applyEnvOverrides() (belt and braces for callers that build partial configs).
67
+ */
68
+ function storeOptionsFromConfig(config) {
69
+ return {
70
+ backend: config.credentials.backend,
71
+ encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
72
+ vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
73
+ vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
74
+ vaultNamespace: config.credentials.vaultNamespace,
75
+ vaultMountPath: config.credentials.vaultMountPath,
76
+ onePasswordVault: config.credentials.onePasswordVault,
77
+ onePasswordAccount: config.credentials.onePasswordAccount,
78
+ keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
79
+ keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
80
+ bitwardenFolder: config.credentials.bitwardenFolder,
81
+ bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
82
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId
83
+ };
84
+ }
85
+ /**
86
+ * Build loopback listener options from config for the daemon. Returns undefined
87
+ * unless the loopback listener is explicitly enabled. If it's enabled but no
88
+ * token is configured we refuse to start the listener (an unauthenticated
89
+ * loopback proxy would be an open credential gateway for any local process) and
90
+ * print how to fix it — the UDS proxy still starts normally.
91
+ */
92
+ function loadLoopbackOptions(config) {
93
+ const lb = config.loopback;
94
+ if (!lb?.enabled)
95
+ return undefined;
96
+ if (!lb.token) {
97
+ console.error('Loopback listener is enabled but no token is set — refusing to start it (would be unauthenticated).');
98
+ console.error('Fix: run `aquaman hermes setup` to generate one, or set AQUAMAN_LOOPBACK_TOKEN.');
99
+ return undefined;
100
+ }
101
+ return { port: lb.port || DEFAULT_LOOPBACK_PORT, token: lb.token, host: lb.host || '127.0.0.1' };
102
+ }
103
+ /**
104
+ * Probe the loopback listener's /_health endpoint (no token required). Returns
105
+ * true if it responds 200 within a short timeout, false otherwise.
106
+ */
107
+ function probeLoopbackHealth(host, port, timeoutMs = 1500) {
108
+ return new Promise((resolve) => {
109
+ const req = http.get({ host, port, path: '/_health', timeout: timeoutMs }, (res) => {
110
+ res.resume();
111
+ resolve(res.statusCode === 200);
112
+ });
113
+ req.on('error', () => resolve(false));
114
+ req.on('timeout', () => { req.destroy(); resolve(false); });
115
+ });
116
+ }
40
117
  // PID file management
41
118
  const getPidFile = () => path.join(getConfigDir(), 'daemon.pid');
42
119
  const writePidFile = () => {
@@ -108,7 +185,7 @@ async function promptSecretInput(prompt) {
108
185
  const program = new Command();
109
186
  program
110
187
  .name('aquaman')
111
- .description('Credential isolation for AI agents \u2014 vault for the proxy core, OpenClaw plugin path, coding-agent adapter path')
188
+ .description('Credential isolation for AI agents \u2014 vault for the proxy core, OpenClaw plugin path, coding-agent adapter path, Hermes loopback path')
112
189
  .version(VERSION)
113
190
  .configureHelp({
114
191
  subcommandTerm(cmd) {
@@ -200,6 +277,21 @@ program
200
277
  lines.push(` ${'(delegates to the aquaman-coder binary; install: npm install -g aquaman-coder)'}`);
201
278
  lines.push('');
202
279
  }
280
+ // --- Hermes namespace (nested) ---
281
+ const hm = byName.get('hermes');
282
+ if (hm) {
283
+ lines.push(aqua('Hermes integration'));
284
+ const hmOrder = ['setup', 'doctor', 'status', 'configure'];
285
+ const hmSubs = helper.visibleCommands(hm).filter((s) => s.name() !== 'help');
286
+ const hmSorted = [
287
+ ...hmOrder.map((n) => hmSubs.find((s) => s.name() === n)).filter(Boolean),
288
+ ...hmSubs.filter((s) => !hmOrder.includes(s.name())),
289
+ ];
290
+ for (const sub of hmSorted) {
291
+ lines.push(renderRow(`hermes ${sub.name()}`, sub.description() || ''));
292
+ }
293
+ lines.push('');
294
+ }
203
295
  // --- Other ---
204
296
  const helpCmd = byName.get('help');
205
297
  if (helpCmd) {
@@ -262,21 +354,10 @@ program
262
354
  // Vault \u2014 backend + creds
263
355
  try {
264
356
  const config = loadConfig();
265
- const store = await createCredentialStore({
266
- backend: config.credentials.backend,
267
- encryptionPassword: config.credentials.encryptionPassword,
268
- vaultAddress: config.credentials.vaultAddress,
269
- vaultToken: config.credentials.vaultToken,
270
- onePasswordVault: config.credentials.onePasswordVault,
271
- onePasswordAccount: config.credentials.onePasswordAccount,
272
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
273
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
274
- bitwardenFolder: config.credentials.bitwardenFolder,
275
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
276
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
277
- });
357
+ const store = await createCredentialStore(storeOptionsFromConfig(config));
278
358
  const creds = await store.list();
279
359
  console.log(` \u2713 ${config.credentials.backend} backend (${creds.length} credential${creds.length !== 1 ? 's' : ''})`);
360
+ printOnePasswordModeHint(config);
280
361
  }
281
362
  catch (err) {
282
363
  console.log(` \u2717 Backend not accessible: ${err.message}`);
@@ -349,6 +430,37 @@ program
349
430
  console.log(` Your coding agents (Claude Code, Codex, \u2026) could use the same`);
350
431
  console.log(` vault protection. Install: ${aqua('npm install -g aquaman-coder')}`);
351
432
  }
433
+ // Hermes integration
434
+ const hermesStateDir = process.env['HERMES_HOME'] || path.join(os.homedir(), '.hermes');
435
+ let hermesDetected = false;
436
+ try {
437
+ const { execSync } = await import('node:child_process');
438
+ execSync('which hermes', { stdio: 'pipe' });
439
+ hermesDetected = true;
440
+ }
441
+ catch { /* */ }
442
+ if (!hermesDetected)
443
+ hermesDetected = fs.existsSync(hermesStateDir);
444
+ console.log('');
445
+ console.log(` ${aqua('Hermes integration')}`);
446
+ if (!hermesDetected) {
447
+ console.log(` \u2022 not detected (skipping \u2014 only relevant if you run the Hermes agent host)`);
448
+ }
449
+ else {
450
+ let loopbackEnabled = false;
451
+ try {
452
+ loopbackEnabled = loadConfig().loopback?.enabled === true;
453
+ }
454
+ catch { /* */ }
455
+ if (loopbackEnabled) {
456
+ console.log(` \u2713 loopback listener enabled (deep: ${aqua('aquaman hermes doctor')})`);
457
+ }
458
+ else {
459
+ console.log(` \u2022 not configured`);
460
+ console.log(` Isolate Hermes' provider keys via an opt-in loopback listener.`);
461
+ console.log(` Run: ${aqua('aquaman hermes setup')}`);
462
+ }
463
+ }
352
464
  console.log('');
353
465
  if (issues === 0) {
354
466
  console.log(' All baseline checks passed.');
@@ -449,19 +561,7 @@ async function runVaultSetup(opts) {
449
561
  fs.mkdirSync(auditDir, { recursive: true, mode: 0o700 });
450
562
  let store;
451
563
  try {
452
- store = await createCredentialStore({
453
- backend: config.credentials.backend,
454
- encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
455
- vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
456
- vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
457
- onePasswordVault: config.credentials.onePasswordVault,
458
- onePasswordAccount: config.credentials.onePasswordAccount,
459
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
460
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
461
- bitwardenFolder: config.credentials.bitwardenFolder,
462
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
463
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
464
- });
564
+ store = await createCredentialStore(storeOptionsFromConfig(config));
465
565
  }
466
566
  catch (err) {
467
567
  console.error(` Failed to initialize ${backend}: ${err instanceof Error ? err.message : err}`);
@@ -587,21 +687,7 @@ openclaw
587
687
  // Initialize credential store
588
688
  let credentialStore;
589
689
  try {
590
- credentialStore = await createCredentialStore({
591
- backend: config.credentials.backend,
592
- encryptionPassword: config.credentials.encryptionPassword,
593
- vaultAddress: config.credentials.vaultAddress,
594
- vaultToken: config.credentials.vaultToken,
595
- vaultNamespace: config.credentials.vaultNamespace,
596
- vaultMountPath: config.credentials.vaultMountPath,
597
- onePasswordVault: config.credentials.onePasswordVault,
598
- onePasswordAccount: config.credentials.onePasswordAccount,
599
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
600
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
601
- bitwardenFolder: config.credentials.bitwardenFolder,
602
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
603
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
604
- });
690
+ credentialStore = wrapWithCache(await createCredentialStore(storeOptionsFromConfig(config)), resolveCacheTtl(config));
605
691
  }
606
692
  catch (err) {
607
693
  console.error(`Credential backend "${config.credentials.backend}" failed to initialize: ${err instanceof Error ? err.message : err}`);
@@ -619,6 +705,7 @@ openclaw
619
705
  allowedServices: config.credentials.proxiedServices,
620
706
  serviceRegistry,
621
707
  policyConfig,
708
+ loopback: loadLoopbackOptions(config),
622
709
  onRequest: (info) => {
623
710
  auditLogger.logCredentialAccess('system', 'system', {
624
711
  service: info.service,
@@ -742,21 +829,7 @@ program
742
829
  // Initialize credential store
743
830
  let credentialStore;
744
831
  try {
745
- credentialStore = await createCredentialStore({
746
- backend: config.credentials.backend,
747
- encryptionPassword: config.credentials.encryptionPassword,
748
- vaultAddress: config.credentials.vaultAddress,
749
- vaultToken: config.credentials.vaultToken,
750
- vaultNamespace: config.credentials.vaultNamespace,
751
- vaultMountPath: config.credentials.vaultMountPath,
752
- onePasswordVault: config.credentials.onePasswordVault,
753
- onePasswordAccount: config.credentials.onePasswordAccount,
754
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
755
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
756
- bitwardenFolder: config.credentials.bitwardenFolder,
757
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
758
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
759
- });
832
+ credentialStore = wrapWithCache(await createCredentialStore(storeOptionsFromConfig(config)), resolveCacheTtl(config));
760
833
  }
761
834
  catch (err) {
762
835
  console.error(`Credential backend "${config.credentials.backend}" failed to initialize: ${err instanceof Error ? err.message : err}`);
@@ -773,6 +846,7 @@ program
773
846
  allowedServices: config.credentials.proxiedServices,
774
847
  serviceRegistry,
775
848
  policyConfig: policyConfig2,
849
+ loopback: loadLoopbackOptions(config),
776
850
  onRequest: (info) => {
777
851
  auditLogger.logCredentialAccess('system', 'system', {
778
852
  service: info.service,
@@ -815,21 +889,7 @@ openclaw
815
889
  // Initialize credential store
816
890
  let credentialStore;
817
891
  try {
818
- credentialStore = await createCredentialStore({
819
- backend: config.credentials.backend,
820
- encryptionPassword: config.credentials.encryptionPassword,
821
- vaultAddress: config.credentials.vaultAddress,
822
- vaultToken: config.credentials.vaultToken,
823
- vaultNamespace: config.credentials.vaultNamespace,
824
- vaultMountPath: config.credentials.vaultMountPath,
825
- onePasswordVault: config.credentials.onePasswordVault,
826
- onePasswordAccount: config.credentials.onePasswordAccount,
827
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
828
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
829
- bitwardenFolder: config.credentials.bitwardenFolder,
830
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
831
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
832
- });
892
+ credentialStore = wrapWithCache(await createCredentialStore(storeOptionsFromConfig(config)), resolveCacheTtl(config));
833
893
  }
834
894
  catch (err) {
835
895
  console.error(`Credential backend "${config.credentials.backend}" failed to initialize: ${err instanceof Error ? err.message : err}`);
@@ -852,6 +912,7 @@ openclaw
852
912
  allowedServices: config.credentials.proxiedServices,
853
913
  serviceRegistry,
854
914
  policyConfig: policyConfig3,
915
+ loopback: loadLoopbackOptions(config),
855
916
  onRequest: (info) => {
856
917
  auditLogger.logCredentialAccess('system', 'system', {
857
918
  service: info.service,
@@ -914,6 +975,248 @@ openclaw
914
975
  console.log(`Configuration written to: ${result}`);
915
976
  }
916
977
  });
978
+ // ── aquaman hermes … — Hermes integration via loopback listener ──
979
+ const hermes = program
980
+ .command('hermes')
981
+ .description('Hermes integration via opt-in loopback listener');
982
+ /** Mask a loopback token for display: keep the prefix + last 4 chars. */
983
+ function maskToken(token) {
984
+ if (token.length <= 12)
985
+ return '****';
986
+ return `${token.slice(0, 7)}…${token.slice(-4)}`;
987
+ }
988
+ // hermes setup — enable loopback listener, generate token, wire ~/.hermes/.env
989
+ hermes
990
+ .command('setup')
991
+ .description('Enable the loopback listener, generate a token, and wire ~/.hermes/.env for Hermes')
992
+ .option('--port <port>', 'Loopback listener port')
993
+ .option('--dry-run', 'Show what would change without writing')
994
+ .option('--no-write', 'Configure the proxy but do not write ~/.hermes/.env')
995
+ .action(async (options) => {
996
+ const config = loadConfig();
997
+ // Resolve port: flag > existing config > default.
998
+ let port = config.loopback?.port || DEFAULT_LOOPBACK_PORT;
999
+ if (options.port) {
1000
+ const p = Number(options.port);
1001
+ if (!Number.isInteger(p) || p <= 0 || p >= 65536) {
1002
+ console.error(`Invalid --port: ${options.port}`);
1003
+ process.exit(1);
1004
+ }
1005
+ port = p;
1006
+ }
1007
+ const host = config.loopback?.host || '127.0.0.1';
1008
+ const token = config.loopback?.token || generateLoopbackToken();
1009
+ // Which services get wired (anthropic/openai today).
1010
+ const integration = createHermesIntegration({
1011
+ port, token, host,
1012
+ services: config.credentials.proxiedServices,
1013
+ config: config.hermes,
1014
+ });
1015
+ const wired = integration.wiredServices();
1016
+ const env = integration.configureHermes();
1017
+ if (wired.length === 0) {
1018
+ console.log('No Hermes-supported services (anthropic, openai) are in your proxiedServices list.');
1019
+ console.log('Add one with: aquaman credentials add anthropic api_key');
1020
+ }
1021
+ if (options.dryRun) {
1022
+ console.log('Dry run — would apply this configuration:\n');
1023
+ console.log(` Loopback listener: http://${host}:${port} (enabled)`);
1024
+ console.log(` Token: ${maskToken(token)}`);
1025
+ console.log(` Wired services: ${wired.join(', ') || '(none)'}`);
1026
+ console.log(` ~/.hermes/.env: ${integration.envPath()}`);
1027
+ console.log('\nEnv vars:');
1028
+ console.log(integration.getEnvForDisplay());
1029
+ return;
1030
+ }
1031
+ // Persist loopback config (enabled + port + token + host).
1032
+ config.loopback = { enabled: true, port, host, token };
1033
+ saveConfig(config);
1034
+ console.log(`✓ Loopback listener enabled on http://${host}:${port} (saved to ${path.join(getConfigDir(), 'config.yaml')})`);
1035
+ if (options.write !== false) {
1036
+ const dest = integration.writeConfiguration(env);
1037
+ console.log(`✓ Wrote Hermes env block to ${dest}`);
1038
+ }
1039
+ else {
1040
+ console.log('Skipped writing ~/.hermes/.env (--no-write). Env vars:');
1041
+ console.log(integration.getEnvForDisplay());
1042
+ }
1043
+ const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
1044
+ console.log('');
1045
+ if (info.installed) {
1046
+ console.log(`✓ Hermes detected (v${info.version})`);
1047
+ }
1048
+ else {
1049
+ console.log('• Hermes CLI not found on PATH. Install: uv tool install hermes-agent');
1050
+ }
1051
+ console.log('\nNext steps:');
1052
+ console.log(` 1. Add credentials: ${aqua('aquaman credentials add anthropic api_key')}`);
1053
+ console.log(` 2. Start the daemon: ${aqua('aquaman daemon')}`);
1054
+ console.log(` 3. Verify: ${aqua('aquaman hermes doctor')}`);
1055
+ });
1056
+ // hermes status — loopback config + env file + Hermes detection (overview)
1057
+ hermes
1058
+ .command('status')
1059
+ .description('Show loopback listener config, ~/.hermes/.env wiring, and Hermes detection')
1060
+ .action(async () => {
1061
+ const config = loadConfig();
1062
+ const lb = config.loopback;
1063
+ console.log('Hermes integration status\n');
1064
+ if (lb?.enabled) {
1065
+ console.log(` Loopback listener: ${aqua('enabled')} http://${lb.host || '127.0.0.1'}:${lb.port || DEFAULT_LOOPBACK_PORT}`);
1066
+ console.log(` Token: ${lb.token ? maskToken(lb.token) : aqua('MISSING — run: aquaman hermes setup')}`);
1067
+ }
1068
+ else {
1069
+ console.log(` Loopback listener: disabled (enable with: ${aqua('aquaman hermes setup')})`);
1070
+ }
1071
+ const integration = createHermesIntegration({
1072
+ port: lb?.port || DEFAULT_LOOPBACK_PORT,
1073
+ token: lb?.token || '',
1074
+ host: lb?.host || '127.0.0.1',
1075
+ services: config.credentials.proxiedServices,
1076
+ config: config.hermes,
1077
+ });
1078
+ console.log(` Wired services: ${integration.wiredServices().join(', ') || '(none)'}`);
1079
+ const envPath = integration.envPath();
1080
+ let envWired = false;
1081
+ try {
1082
+ envWired = fs.existsSync(envPath) && fs.readFileSync(envPath, 'utf-8').includes('aquaman managed');
1083
+ }
1084
+ catch { /* unreadable */ }
1085
+ console.log(` ~/.hermes/.env: ${envWired ? aqua('wired') : 'not wired'} (${envPath})`);
1086
+ const shadowed = managedScopeShadowedKeys(integration.configureHermes());
1087
+ if (shadowed.length > 0) {
1088
+ console.log(` Managed scope: ⚠ ${HERMES_MANAGED_ENV_PATH} pins ${shadowed.join(', ')} — proxy bypassed for these keys`);
1089
+ }
1090
+ const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
1091
+ console.log(` Hermes CLI: ${info.installed ? aqua(`v${info.version}`) : 'not found'}`);
1092
+ // Live liveness probe of the loopback listener (no token needed for _health).
1093
+ if (lb?.enabled) {
1094
+ const alive = await probeLoopbackHealth(lb.host || '127.0.0.1', lb.port || DEFAULT_LOOPBACK_PORT);
1095
+ console.log(` Listener: ${alive ? aqua('responding') : 'not responding (is the daemon running?)'}`);
1096
+ }
1097
+ });
1098
+ // hermes doctor — deep diagnostic; exit 1 on any failure
1099
+ hermes
1100
+ .command('doctor')
1101
+ .description('Deep diagnostic — loopback config, listener reachability, env wiring, per-service vault credentials')
1102
+ .action(async () => {
1103
+ const config = loadConfig();
1104
+ const lb = config.loopback;
1105
+ let ok = true;
1106
+ const fail = (msg) => { ok = false; console.log(` ✗ ${msg}`); };
1107
+ const pass = (msg) => console.log(` ✓ ${msg}`);
1108
+ console.log('Hermes integration diagnostic\n');
1109
+ // 1. Loopback enabled + token present
1110
+ if (!lb?.enabled) {
1111
+ fail('Loopback listener is disabled — run: aquaman hermes setup');
1112
+ }
1113
+ else {
1114
+ pass(`Loopback listener enabled (http://${lb.host || '127.0.0.1'}:${lb.port || DEFAULT_LOOPBACK_PORT})`);
1115
+ if (!lb.token)
1116
+ fail('Loopback token missing — run: aquaman hermes setup');
1117
+ else
1118
+ pass('Loopback token configured');
1119
+ }
1120
+ // 2. Listener reachable
1121
+ if (lb?.enabled) {
1122
+ const alive = await probeLoopbackHealth(lb.host || '127.0.0.1', lb.port || DEFAULT_LOOPBACK_PORT);
1123
+ if (alive)
1124
+ pass('Loopback listener responding to /_health');
1125
+ else
1126
+ fail('Loopback listener not responding — start the daemon: aquaman daemon');
1127
+ }
1128
+ // 3. ~/.hermes/.env wired
1129
+ const integration = createHermesIntegration({
1130
+ port: lb?.port || DEFAULT_LOOPBACK_PORT,
1131
+ token: lb?.token || '',
1132
+ host: lb?.host || '127.0.0.1',
1133
+ services: config.credentials.proxiedServices,
1134
+ config: config.hermes,
1135
+ });
1136
+ const envPath = integration.envPath();
1137
+ try {
1138
+ if (fs.existsSync(envPath) && fs.readFileSync(envPath, 'utf-8').includes('aquaman managed')) {
1139
+ pass(`~/.hermes/.env has the aquaman block (${envPath})`);
1140
+ }
1141
+ else {
1142
+ fail(`~/.hermes/.env not wired — run: aquaman hermes setup`);
1143
+ }
1144
+ }
1145
+ catch {
1146
+ fail(`Could not read ${envPath}`);
1147
+ }
1148
+ // 3b. Hermes >=0.17 managed scope: /etc/hermes/.env overrides ~/.hermes/.env
1149
+ {
1150
+ const shadowed = managedScopeShadowedKeys(integration.configureHermes());
1151
+ if (shadowed.length > 0) {
1152
+ fail(`Hermes managed scope (${HERMES_MANAGED_ENV_PATH}) pins ${shadowed.join(', ')} — it overrides ~/.hermes/.env, so the proxy is BYPASSED for these keys. Remove them from the managed file or point them at the proxy.`);
1153
+ }
1154
+ else {
1155
+ pass('No managed-scope override of aquaman env vars');
1156
+ }
1157
+ }
1158
+ // 4. Per-service vault credentials resolve
1159
+ const wired = integration.wiredServices();
1160
+ if (wired.length === 0) {
1161
+ fail('No Hermes-supported services configured (anthropic, openai)');
1162
+ }
1163
+ else {
1164
+ try {
1165
+ const store = await createCredentialStore(storeOptionsFromConfig(config));
1166
+ for (const svc of wired) {
1167
+ const val = await store.get(svc, 'api_key');
1168
+ if (val)
1169
+ pass(`Vault credential ${svc}/api_key resolves`);
1170
+ else
1171
+ fail(`No credential for ${svc}/api_key — run: aquaman credentials add ${svc} api_key`);
1172
+ }
1173
+ printOnePasswordModeHint(config, ' ');
1174
+ }
1175
+ catch (err) {
1176
+ fail(`Vault backend "${config.credentials.backend}" failed: ${err instanceof Error ? err.message : err}`);
1177
+ }
1178
+ }
1179
+ // 5. Hermes installed
1180
+ const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
1181
+ if (info.installed)
1182
+ pass(`Hermes CLI installed (v${info.version})`);
1183
+ else
1184
+ console.log(' • Hermes CLI not found on PATH (install: uv tool install hermes-agent)');
1185
+ console.log('');
1186
+ console.log(ok ? aqua('All checks passed.') : 'Some checks failed — see above.');
1187
+ process.exit(ok ? 0 : 1);
1188
+ });
1189
+ // hermes configure — emit/write the Hermes env vars without touching daemon config
1190
+ hermes
1191
+ .command('configure')
1192
+ .description('Generate the Hermes env vars (base URLs + placeholder key) for ~/.hermes/.env')
1193
+ .option('--method <method>', 'Output method: env (display) or dotenv (write ~/.hermes/.env)', 'env')
1194
+ .option('--dry-run', 'Show configuration without writing')
1195
+ .action(async (options) => {
1196
+ const config = loadConfig();
1197
+ const lb = config.loopback;
1198
+ if (!lb?.token) {
1199
+ console.error('No loopback token configured. Run `aquaman hermes setup` first.');
1200
+ process.exit(1);
1201
+ }
1202
+ const integration = createHermesIntegration({
1203
+ port: lb.port || DEFAULT_LOOPBACK_PORT,
1204
+ token: lb.token,
1205
+ host: lb.host || '127.0.0.1',
1206
+ services: config.credentials.proxiedServices,
1207
+ config: { configMethod: options.method === 'dotenv' ? 'dotenv' : 'env', binaryPath: config.hermes?.binaryPath },
1208
+ });
1209
+ const env = integration.configureHermes();
1210
+ if (options.dryRun || options.method !== 'dotenv') {
1211
+ console.log('Hermes env vars (add to ~/.hermes/.env):\n');
1212
+ for (const [k, v] of Object.entries(env))
1213
+ console.log(`${k}=${v}`);
1214
+ }
1215
+ else {
1216
+ const dest = integration.writeConfiguration(env);
1217
+ console.log(`Configuration written to: ${dest}`);
1218
+ }
1219
+ });
917
1220
  // Init command
918
1221
  program
919
1222
  .command('init')
@@ -1056,7 +1359,7 @@ openclaw
1056
1359
  process.exit(1);
1057
1360
  }
1058
1361
  }
1059
- catch (err) {
1362
+ catch {
1060
1363
  console.error(` Cannot reach Vault at ${vaultAddr}.`);
1061
1364
  process.exit(1);
1062
1365
  }
@@ -1136,19 +1439,7 @@ openclaw
1136
1439
  // 3. Prompt for API keys (or read from env in non-interactive mode)
1137
1440
  let store;
1138
1441
  try {
1139
- store = await createCredentialStore({
1140
- backend: config.credentials.backend,
1141
- encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
1142
- vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
1143
- vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
1144
- onePasswordVault: config.credentials.onePasswordVault,
1145
- onePasswordAccount: config.credentials.onePasswordAccount,
1146
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
1147
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
1148
- bitwardenFolder: config.credentials.bitwardenFolder,
1149
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
1150
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
1151
- });
1442
+ store = await createCredentialStore(storeOptionsFromConfig(config));
1152
1443
  }
1153
1444
  catch (err) {
1154
1445
  console.error(` Failed to initialize ${backend} credential store: ${err instanceof Error ? err.message : err}`);