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.
- package/README.md +12 -12
- package/dist/cli/index.js +373 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils/config.d.ts +8 -0
- package/dist/core/utils/config.d.ts.map +1 -1
- package/dist/core/utils/config.js +49 -1
- package/dist/core/utils/config.js.map +1 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/core/utils/index.js.map +1 -1
- package/dist/daemon.d.ts +26 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +104 -6
- package/dist/daemon.js.map +1 -1
- package/dist/hermes/config-writer.d.ts +61 -0
- package/dist/hermes/config-writer.d.ts.map +1 -0
- package/dist/hermes/config-writer.js +112 -0
- package/dist/hermes/config-writer.js.map +1 -0
- package/dist/hermes/integration.d.ts +53 -0
- package/dist/hermes/integration.d.ts.map +1 -0
- package/dist/hermes/integration.js +85 -0
- package/dist/hermes/integration.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/openclaw/integration.d.ts +13 -0
- package/dist/openclaw/integration.d.ts.map +1 -1
- package/dist/openclaw/integration.js +30 -0
- package/dist/openclaw/integration.js.map +1 -1
- 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
|
|
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
|
|
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
|
|
61
|
-
- **Service allowlisting
|
|
62
|
-
- **Request policies
|
|
63
|
-
- **Audit trail
|
|
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
|
-
|
|
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
|
|
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)
|
|
80
|
-
- **[`docs/PACKAGES.md`](../../docs/PACKAGES.md)
|
|
81
|
-
- **[`docs/compliance/`](../../docs/compliance/)
|
|
82
|
-
- **[`CLAUDE.md`](../../CLAUDE.md)
|
|
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
|
|
1699
|
-
|
|
1700
|
-
|
|
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 {
|