aquaman-proxy 0.12.0 → 0.13.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 (38) hide show
  1. package/README.md +12 -12
  2. package/dist/cli/index.js +373 -6
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/index.d.ts +1 -1
  5. package/dist/core/index.d.ts.map +1 -1
  6. package/dist/core/index.js +1 -1
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/types.d.ts +25 -0
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/core/utils/config.d.ts +8 -0
  11. package/dist/core/utils/config.d.ts.map +1 -1
  12. package/dist/core/utils/config.js +49 -1
  13. package/dist/core/utils/config.js.map +1 -1
  14. package/dist/core/utils/index.d.ts +1 -1
  15. package/dist/core/utils/index.d.ts.map +1 -1
  16. package/dist/core/utils/index.js +1 -1
  17. package/dist/core/utils/index.js.map +1 -1
  18. package/dist/daemon.d.ts +26 -0
  19. package/dist/daemon.d.ts.map +1 -1
  20. package/dist/daemon.js +104 -6
  21. package/dist/daemon.js.map +1 -1
  22. package/dist/hermes/config-writer.d.ts +61 -0
  23. package/dist/hermes/config-writer.d.ts.map +1 -0
  24. package/dist/hermes/config-writer.js +112 -0
  25. package/dist/hermes/config-writer.js.map +1 -0
  26. package/dist/hermes/integration.d.ts +53 -0
  27. package/dist/hermes/integration.d.ts.map +1 -0
  28. package/dist/hermes/integration.js +85 -0
  29. package/dist/hermes/integration.js.map +1 -0
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/openclaw/integration.d.ts +13 -0
  35. package/dist/openclaw/integration.d.ts.map +1 -1
  36. package/dist/openclaw/integration.js +30 -0
  37. package/dist/openclaw/integration.js.map +1 -1
  38. package/package.json +2 -2
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 │
@@ -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,12 @@ 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 } 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
- import { createOpenClawIntegration } from '../openclaw/integration.js';
19
+ import { createOpenClawIntegration, authProfilesAreSqliteOnly } from '../openclaw/integration.js';
20
+ import { createHermesIntegration, detectHermes } from '../hermes/integration.js';
20
21
  import { loadPolicyFromConfig, validatePolicyConfig, getDefaultPolicyPresets, matchPolicy } from '../request-policy.js';
21
22
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
22
23
  // Read version from package.json (single source of truth)
@@ -37,6 +38,38 @@ function validateCredName(label, value) {
37
38
  process.exit(1);
38
39
  }
39
40
  }
41
+ /**
42
+ * Build loopback listener options from config for the daemon. Returns undefined
43
+ * unless the loopback listener is explicitly enabled. If it's enabled but no
44
+ * token is configured we refuse to start the listener (an unauthenticated
45
+ * loopback proxy would be an open credential gateway for any local process) and
46
+ * print how to fix it — the UDS proxy still starts normally.
47
+ */
48
+ function loadLoopbackOptions(config) {
49
+ const lb = config.loopback;
50
+ if (!lb?.enabled)
51
+ return undefined;
52
+ if (!lb.token) {
53
+ console.error('Loopback listener is enabled but no token is set — refusing to start it (would be unauthenticated).');
54
+ console.error('Fix: run `aquaman hermes setup` to generate one, or set AQUAMAN_LOOPBACK_TOKEN.');
55
+ return undefined;
56
+ }
57
+ return { port: lb.port || DEFAULT_LOOPBACK_PORT, token: lb.token, host: lb.host || '127.0.0.1' };
58
+ }
59
+ /**
60
+ * Probe the loopback listener's /_health endpoint (no token required). Returns
61
+ * true if it responds 200 within a short timeout, false otherwise.
62
+ */
63
+ function probeLoopbackHealth(host, port, timeoutMs = 1500) {
64
+ return new Promise((resolve) => {
65
+ const req = http.get({ host, port, path: '/_health', timeout: timeoutMs }, (res) => {
66
+ res.resume();
67
+ resolve(res.statusCode === 200);
68
+ });
69
+ req.on('error', () => resolve(false));
70
+ req.on('timeout', () => { req.destroy(); resolve(false); });
71
+ });
72
+ }
40
73
  // PID file management
41
74
  const getPidFile = () => path.join(getConfigDir(), 'daemon.pid');
42
75
  const writePidFile = () => {
@@ -108,7 +141,7 @@ async function promptSecretInput(prompt) {
108
141
  const program = new Command();
109
142
  program
110
143
  .name('aquaman')
111
- .description('Credential isolation for AI agents \u2014 vault for the proxy core, OpenClaw plugin path, coding-agent adapter path')
144
+ .description('Credential isolation for AI agents \u2014 vault for the proxy core, OpenClaw plugin path, coding-agent adapter path, Hermes loopback path')
112
145
  .version(VERSION)
113
146
  .configureHelp({
114
147
  subcommandTerm(cmd) {
@@ -200,6 +233,21 @@ program
200
233
  lines.push(` ${'(delegates to the aquaman-coder binary; install: npm install -g aquaman-coder)'}`);
201
234
  lines.push('');
202
235
  }
236
+ // --- Hermes namespace (nested) ---
237
+ const hm = byName.get('hermes');
238
+ if (hm) {
239
+ lines.push(aqua('Hermes integration'));
240
+ const hmOrder = ['setup', 'doctor', 'status', 'configure'];
241
+ const hmSubs = helper.visibleCommands(hm).filter((s) => s.name() !== 'help');
242
+ const hmSorted = [
243
+ ...hmOrder.map((n) => hmSubs.find((s) => s.name() === n)).filter(Boolean),
244
+ ...hmSubs.filter((s) => !hmOrder.includes(s.name())),
245
+ ];
246
+ for (const sub of hmSorted) {
247
+ lines.push(renderRow(`hermes ${sub.name()}`, sub.description() || ''));
248
+ }
249
+ lines.push('');
250
+ }
203
251
  // --- Other ---
204
252
  const helpCmd = byName.get('help');
205
253
  if (helpCmd) {
@@ -349,6 +397,37 @@ program
349
397
  console.log(` Your coding agents (Claude Code, Codex, \u2026) could use the same`);
350
398
  console.log(` vault protection. Install: ${aqua('npm install -g aquaman-coder')}`);
351
399
  }
400
+ // Hermes integration
401
+ const hermesStateDir = process.env['HERMES_HOME'] || path.join(os.homedir(), '.hermes');
402
+ let hermesDetected = false;
403
+ try {
404
+ const { execSync } = await import('node:child_process');
405
+ execSync('which hermes', { stdio: 'pipe' });
406
+ hermesDetected = true;
407
+ }
408
+ catch { /* */ }
409
+ if (!hermesDetected)
410
+ hermesDetected = fs.existsSync(hermesStateDir);
411
+ console.log('');
412
+ console.log(` ${aqua('Hermes integration')}`);
413
+ if (!hermesDetected) {
414
+ console.log(` \u2022 not detected (skipping \u2014 only relevant if you run the Hermes agent host)`);
415
+ }
416
+ else {
417
+ let loopbackEnabled = false;
418
+ try {
419
+ loopbackEnabled = loadConfig().loopback?.enabled === true;
420
+ }
421
+ catch { /* */ }
422
+ if (loopbackEnabled) {
423
+ console.log(` \u2713 loopback listener enabled (deep: ${aqua('aquaman hermes doctor')})`);
424
+ }
425
+ else {
426
+ console.log(` \u2022 not configured`);
427
+ console.log(` Isolate Hermes' provider keys via an opt-in loopback listener.`);
428
+ console.log(` Run: ${aqua('aquaman hermes setup')}`);
429
+ }
430
+ }
352
431
  console.log('');
353
432
  if (issues === 0) {
354
433
  console.log(' All baseline checks passed.');
@@ -619,6 +698,7 @@ openclaw
619
698
  allowedServices: config.credentials.proxiedServices,
620
699
  serviceRegistry,
621
700
  policyConfig,
701
+ loopback: loadLoopbackOptions(config),
622
702
  onRequest: (info) => {
623
703
  auditLogger.logCredentialAccess('system', 'system', {
624
704
  service: info.service,
@@ -773,6 +853,7 @@ program
773
853
  allowedServices: config.credentials.proxiedServices,
774
854
  serviceRegistry,
775
855
  policyConfig: policyConfig2,
856
+ loopback: loadLoopbackOptions(config),
776
857
  onRequest: (info) => {
777
858
  auditLogger.logCredentialAccess('system', 'system', {
778
859
  service: info.service,
@@ -852,6 +933,7 @@ openclaw
852
933
  allowedServices: config.credentials.proxiedServices,
853
934
  serviceRegistry,
854
935
  policyConfig: policyConfig3,
936
+ loopback: loadLoopbackOptions(config),
855
937
  onRequest: (info) => {
856
938
  auditLogger.logCredentialAccess('system', 'system', {
857
939
  service: info.service,
@@ -914,6 +996,247 @@ openclaw
914
996
  console.log(`Configuration written to: ${result}`);
915
997
  }
916
998
  });
999
+ // ── aquaman hermes … — Hermes integration via loopback listener ──
1000
+ const hermes = program
1001
+ .command('hermes')
1002
+ .description('Hermes integration via opt-in loopback listener');
1003
+ /** Mask a loopback token for display: keep the prefix + last 4 chars. */
1004
+ function maskToken(token) {
1005
+ if (token.length <= 12)
1006
+ return '****';
1007
+ return `${token.slice(0, 7)}…${token.slice(-4)}`;
1008
+ }
1009
+ // hermes setup — enable loopback listener, generate token, wire ~/.hermes/.env
1010
+ hermes
1011
+ .command('setup')
1012
+ .description('Enable the loopback listener, generate a token, and wire ~/.hermes/.env for Hermes')
1013
+ .option('--port <port>', 'Loopback listener port')
1014
+ .option('--dry-run', 'Show what would change without writing')
1015
+ .option('--no-write', 'Configure the proxy but do not write ~/.hermes/.env')
1016
+ .action(async (options) => {
1017
+ const config = loadConfig();
1018
+ // Resolve port: flag > existing config > default.
1019
+ let port = config.loopback?.port || DEFAULT_LOOPBACK_PORT;
1020
+ if (options.port) {
1021
+ const p = Number(options.port);
1022
+ if (!Number.isInteger(p) || p <= 0 || p >= 65536) {
1023
+ console.error(`Invalid --port: ${options.port}`);
1024
+ process.exit(1);
1025
+ }
1026
+ port = p;
1027
+ }
1028
+ const host = config.loopback?.host || '127.0.0.1';
1029
+ const token = config.loopback?.token || generateLoopbackToken();
1030
+ // Which services get wired (anthropic/openai today).
1031
+ const integration = createHermesIntegration({
1032
+ port, token, host,
1033
+ services: config.credentials.proxiedServices,
1034
+ config: config.hermes,
1035
+ });
1036
+ const wired = integration.wiredServices();
1037
+ const env = integration.configureHermes();
1038
+ if (wired.length === 0) {
1039
+ console.log('No Hermes-supported services (anthropic, openai) are in your proxiedServices list.');
1040
+ console.log('Add one with: aquaman credentials add anthropic api_key');
1041
+ }
1042
+ if (options.dryRun) {
1043
+ console.log('Dry run — would apply this configuration:\n');
1044
+ console.log(` Loopback listener: http://${host}:${port} (enabled)`);
1045
+ console.log(` Token: ${maskToken(token)}`);
1046
+ console.log(` Wired services: ${wired.join(', ') || '(none)'}`);
1047
+ console.log(` ~/.hermes/.env: ${integration.envPath()}`);
1048
+ console.log('\nEnv vars:');
1049
+ console.log(integration.getEnvForDisplay());
1050
+ return;
1051
+ }
1052
+ // Persist loopback config (enabled + port + token + host).
1053
+ config.loopback = { enabled: true, port, host, token };
1054
+ saveConfig(config);
1055
+ console.log(`✓ Loopback listener enabled on http://${host}:${port} (saved to ${path.join(getConfigDir(), 'config.yaml')})`);
1056
+ if (options.write !== false) {
1057
+ const dest = integration.writeConfiguration(env);
1058
+ console.log(`✓ Wrote Hermes env block to ${dest}`);
1059
+ }
1060
+ else {
1061
+ console.log('Skipped writing ~/.hermes/.env (--no-write). Env vars:');
1062
+ console.log(integration.getEnvForDisplay());
1063
+ }
1064
+ const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
1065
+ console.log('');
1066
+ if (info.installed) {
1067
+ console.log(`✓ Hermes detected (v${info.version})`);
1068
+ }
1069
+ else {
1070
+ console.log('• Hermes CLI not found on PATH. Install: uv tool install hermes-agent');
1071
+ }
1072
+ console.log('\nNext steps:');
1073
+ console.log(` 1. Add credentials: ${aqua('aquaman credentials add anthropic api_key')}`);
1074
+ console.log(` 2. Start the daemon: ${aqua('aquaman daemon')}`);
1075
+ console.log(` 3. Verify: ${aqua('aquaman hermes doctor')}`);
1076
+ });
1077
+ // hermes status — loopback config + env file + Hermes detection (overview)
1078
+ hermes
1079
+ .command('status')
1080
+ .description('Show loopback listener config, ~/.hermes/.env wiring, and Hermes detection')
1081
+ .action(async () => {
1082
+ const config = loadConfig();
1083
+ const lb = config.loopback;
1084
+ console.log('Hermes integration status\n');
1085
+ if (lb?.enabled) {
1086
+ console.log(` Loopback listener: ${aqua('enabled')} http://${lb.host || '127.0.0.1'}:${lb.port || DEFAULT_LOOPBACK_PORT}`);
1087
+ console.log(` Token: ${lb.token ? maskToken(lb.token) : aqua('MISSING — run: aquaman hermes setup')}`);
1088
+ }
1089
+ else {
1090
+ console.log(` Loopback listener: disabled (enable with: ${aqua('aquaman hermes setup')})`);
1091
+ }
1092
+ const integration = createHermesIntegration({
1093
+ port: lb?.port || DEFAULT_LOOPBACK_PORT,
1094
+ token: lb?.token || '',
1095
+ host: lb?.host || '127.0.0.1',
1096
+ services: config.credentials.proxiedServices,
1097
+ config: config.hermes,
1098
+ });
1099
+ console.log(` Wired services: ${integration.wiredServices().join(', ') || '(none)'}`);
1100
+ const envPath = integration.envPath();
1101
+ let envWired = false;
1102
+ try {
1103
+ envWired = fs.existsSync(envPath) && fs.readFileSync(envPath, 'utf-8').includes('aquaman managed');
1104
+ }
1105
+ catch { /* unreadable */ }
1106
+ console.log(` ~/.hermes/.env: ${envWired ? aqua('wired') : 'not wired'} (${envPath})`);
1107
+ const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
1108
+ console.log(` Hermes CLI: ${info.installed ? aqua(`v${info.version}`) : 'not found'}`);
1109
+ // Live liveness probe of the loopback listener (no token needed for _health).
1110
+ if (lb?.enabled) {
1111
+ const alive = await probeLoopbackHealth(lb.host || '127.0.0.1', lb.port || DEFAULT_LOOPBACK_PORT);
1112
+ console.log(` Listener: ${alive ? aqua('responding') : 'not responding (is the daemon running?)'}`);
1113
+ }
1114
+ });
1115
+ // hermes doctor — deep diagnostic; exit 1 on any failure
1116
+ hermes
1117
+ .command('doctor')
1118
+ .description('Deep diagnostic — loopback config, listener reachability, env wiring, per-service vault credentials')
1119
+ .action(async () => {
1120
+ const config = loadConfig();
1121
+ const lb = config.loopback;
1122
+ let ok = true;
1123
+ const fail = (msg) => { ok = false; console.log(` ✗ ${msg}`); };
1124
+ const pass = (msg) => console.log(` ✓ ${msg}`);
1125
+ console.log('Hermes integration diagnostic\n');
1126
+ // 1. Loopback enabled + token present
1127
+ if (!lb?.enabled) {
1128
+ fail('Loopback listener is disabled — run: aquaman hermes setup');
1129
+ }
1130
+ else {
1131
+ pass(`Loopback listener enabled (http://${lb.host || '127.0.0.1'}:${lb.port || DEFAULT_LOOPBACK_PORT})`);
1132
+ if (!lb.token)
1133
+ fail('Loopback token missing — run: aquaman hermes setup');
1134
+ else
1135
+ pass('Loopback token configured');
1136
+ }
1137
+ // 2. Listener reachable
1138
+ if (lb?.enabled) {
1139
+ const alive = await probeLoopbackHealth(lb.host || '127.0.0.1', lb.port || DEFAULT_LOOPBACK_PORT);
1140
+ if (alive)
1141
+ pass('Loopback listener responding to /_health');
1142
+ else
1143
+ fail('Loopback listener not responding — start the daemon: aquaman daemon');
1144
+ }
1145
+ // 3. ~/.hermes/.env wired
1146
+ const integration = createHermesIntegration({
1147
+ port: lb?.port || DEFAULT_LOOPBACK_PORT,
1148
+ token: lb?.token || '',
1149
+ host: lb?.host || '127.0.0.1',
1150
+ services: config.credentials.proxiedServices,
1151
+ config: config.hermes,
1152
+ });
1153
+ const envPath = integration.envPath();
1154
+ try {
1155
+ if (fs.existsSync(envPath) && fs.readFileSync(envPath, 'utf-8').includes('aquaman managed')) {
1156
+ pass(`~/.hermes/.env has the aquaman block (${envPath})`);
1157
+ }
1158
+ else {
1159
+ fail(`~/.hermes/.env not wired — run: aquaman hermes setup`);
1160
+ }
1161
+ }
1162
+ catch {
1163
+ fail(`Could not read ${envPath}`);
1164
+ }
1165
+ // 4. Per-service vault credentials resolve
1166
+ const wired = integration.wiredServices();
1167
+ if (wired.length === 0) {
1168
+ fail('No Hermes-supported services configured (anthropic, openai)');
1169
+ }
1170
+ else {
1171
+ try {
1172
+ const store = await createCredentialStore({
1173
+ backend: config.credentials.backend,
1174
+ encryptionPassword: config.credentials.encryptionPassword,
1175
+ vaultAddress: config.credentials.vaultAddress,
1176
+ vaultToken: config.credentials.vaultToken,
1177
+ vaultNamespace: config.credentials.vaultNamespace,
1178
+ vaultMountPath: config.credentials.vaultMountPath,
1179
+ onePasswordVault: config.credentials.onePasswordVault,
1180
+ onePasswordAccount: config.credentials.onePasswordAccount,
1181
+ keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
1182
+ keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
1183
+ bitwardenFolder: config.credentials.bitwardenFolder,
1184
+ bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
1185
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
1186
+ });
1187
+ for (const svc of wired) {
1188
+ const val = await store.get(svc, 'api_key');
1189
+ if (val)
1190
+ pass(`Vault credential ${svc}/api_key resolves`);
1191
+ else
1192
+ fail(`No credential for ${svc}/api_key — run: aquaman credentials add ${svc} api_key`);
1193
+ }
1194
+ }
1195
+ catch (err) {
1196
+ fail(`Vault backend "${config.credentials.backend}" failed: ${err instanceof Error ? err.message : err}`);
1197
+ }
1198
+ }
1199
+ // 5. Hermes installed
1200
+ const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
1201
+ if (info.installed)
1202
+ pass(`Hermes CLI installed (v${info.version})`);
1203
+ else
1204
+ console.log(' • Hermes CLI not found on PATH (install: uv tool install hermes-agent)');
1205
+ console.log('');
1206
+ console.log(ok ? aqua('All checks passed.') : 'Some checks failed — see above.');
1207
+ process.exit(ok ? 0 : 1);
1208
+ });
1209
+ // hermes configure — emit/write the Hermes env vars without touching daemon config
1210
+ hermes
1211
+ .command('configure')
1212
+ .description('Generate the Hermes env vars (base URLs + placeholder key) for ~/.hermes/.env')
1213
+ .option('--method <method>', 'Output method: env (display) or dotenv (write ~/.hermes/.env)', 'env')
1214
+ .option('--dry-run', 'Show configuration without writing')
1215
+ .action(async (options) => {
1216
+ const config = loadConfig();
1217
+ const lb = config.loopback;
1218
+ if (!lb?.token) {
1219
+ console.error('No loopback token configured. Run `aquaman hermes setup` first.');
1220
+ process.exit(1);
1221
+ }
1222
+ const integration = createHermesIntegration({
1223
+ port: lb.port || DEFAULT_LOOPBACK_PORT,
1224
+ token: lb.token,
1225
+ host: lb.host || '127.0.0.1',
1226
+ services: config.credentials.proxiedServices,
1227
+ config: { configMethod: options.method === 'dotenv' ? 'dotenv' : 'env', binaryPath: config.hermes?.binaryPath },
1228
+ });
1229
+ const env = integration.configureHermes();
1230
+ if (options.dryRun || options.method !== 'dotenv') {
1231
+ console.log('Hermes env vars (add to ~/.hermes/.env):\n');
1232
+ for (const [k, v] of Object.entries(env))
1233
+ console.log(`${k}=${v}`);
1234
+ }
1235
+ else {
1236
+ const dest = integration.writeConfiguration(env);
1237
+ console.log(`Configuration written to: ${dest}`);
1238
+ }
1239
+ });
917
1240
  // Init command
918
1241
  program
919
1242
  .command('init')
@@ -1363,6 +1686,26 @@ openclaw
1363
1686
  fs.mkdirSync(profilesDir, { recursive: true, mode: 0o700 });
1364
1687
  fs.writeFileSync(profilesPath, JSON.stringify({ version: 1, profiles, order }, null, 2), { mode: 0o600 });
1365
1688
  console.log(' \u2713 Auth profiles generated at ' + profilesPath);
1689
+ // OpenClaw >= 2026.6.5 reads provider auth profiles from each agent's
1690
+ // openclaw-agent.sqlite, not auth-profiles.json (openclaw/openclaw#89102).
1691
+ // The placeholder above must be imported into SQLite once with
1692
+ // `openclaw doctor --fix`. We deliberately do NOT auto-run that here:
1693
+ // it is OpenClaw's broad config-healing command \u2014 verified to take
1694
+ // ~19s (gateway probes) and to rewrite openclaw.json (it reset our
1695
+ // plugin entry to bundled defaults in testing). `aquaman openclaw
1696
+ // doctor` detects the SQLite-only case and prints the import step so
1697
+ // the operator runs it intentionally. Holistic fix (SecretRef
1698
+ // provider manifest) is tracked for the next plugin touch.
1699
+ let ocVersion = null;
1700
+ try {
1701
+ const { execSync } = await import('node:child_process');
1702
+ ocVersion = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
1703
+ }
1704
+ catch { /* CLI not on PATH */ }
1705
+ if (authProfilesAreSqliteOnly(ocVersion)) {
1706
+ console.log(' \u2192 OpenClaw \u2265 2026.6.5 reads auth profiles from SQLite; import the placeholder once:');
1707
+ console.log(' openclaw doctor --fix (backs up openclaw.json first; see aquaman openclaw doctor)');
1708
+ }
1366
1709
  }
1367
1710
  }
1368
1711
  }
@@ -1602,6 +1945,7 @@ openclaw
1602
1945
  }
1603
1946
  // 6. OpenClaw detection
1604
1947
  let openclawDetected = false;
1948
+ let openclawVersion = null;
1605
1949
  let cliFound = false;
1606
1950
  try {
1607
1951
  const { execSync } = await import('node:child_process');
@@ -1613,6 +1957,7 @@ openclaw
1613
1957
  try {
1614
1958
  const { execSync } = await import('node:child_process');
1615
1959
  const ver = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
1960
+ openclawVersion = ver;
1616
1961
  console.log(` \u2713 ${aqua('OpenClaw')} installed (${ver})`);
1617
1962
  }
1618
1963
  catch {
@@ -1695,9 +2040,31 @@ openclaw
1695
2040
  // openclaw.json invalid — already reported above
1696
2041
  }
1697
2042
  }
1698
- // 9. Auth profiles exist
1699
- const profilesPath = path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json');
1700
- if (fs.existsSync(profilesPath)) {
2043
+ // 9. Auth profiles. OpenClaw >= 2026.6.5 reads provider auth profiles from
2044
+ // each agent's openclaw-agent.sqlite and no longer reads
2045
+ // auth-profiles.json at runtime (openclaw/openclaw#89102). On those
2046
+ // versions the placeholder the plugin writes to the JSON file must be
2047
+ // imported into SQLite once via `openclaw doctor --fix`.
2048
+ const agentDir = path.join(openclawStateDir, 'agents', 'main', 'agent');
2049
+ const profilesPath = path.join(agentDir, 'auth-profiles.json');
2050
+ const sqlitePath = path.join(agentDir, 'openclaw-agent.sqlite');
2051
+ if (authProfilesAreSqliteOnly(openclawVersion)) {
2052
+ if (fs.existsSync(profilesPath)) {
2053
+ console.log(` \u2717 ${aqua('Auth profiles')} JSON present but ${openclawVersion} reads from SQLite`);
2054
+ console.log(' \u2192 Import the placeholder once: openclaw doctor --fix (backs up openclaw.json first)');
2055
+ console.log(' (OpenClaw \u2265 2026.6.5 dropped the runtime auth-profiles.json read path \u2014 openclaw/openclaw#89102)');
2056
+ issues++;
2057
+ }
2058
+ else if (fs.existsSync(sqlitePath)) {
2059
+ console.log(` \u2713 ${aqua('Auth profiles')} (SQLite store present; OpenClaw \u2265 2026.6.5)`);
2060
+ }
2061
+ else {
2062
+ console.log(` \u2717 ${aqua('Auth profiles')} missing (no SQLite store found)`);
2063
+ console.log(' \u2192 Run: aquaman setup, then: openclaw doctor --fix');
2064
+ issues++;
2065
+ }
2066
+ }
2067
+ else if (fs.existsSync(profilesPath)) {
1701
2068
  console.log(` \u2713 ${aqua('Auth profiles')} exist`);
1702
2069
  }
1703
2070
  else {