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.
- package/README.md +13 -13
- package/dist/cli/index.js +378 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/core/audit/logger.js +1 -1
- package/dist/core/audit/logger.js.map +1 -1
- package/dist/core/credentials/backends/onepassword.js +1 -1
- package/dist/core/credentials/backends/onepassword.js.map +1 -1
- package/dist/core/credentials/backends/systemd-creds.js +1 -1
- package/dist/core/credentials/backends/systemd-creds.js.map +1 -1
- package/dist/core/credentials/caching-store.d.ts +43 -0
- package/dist/core/credentials/caching-store.d.ts.map +1 -0
- package/dist/core/credentials/caching-store.js +77 -0
- package/dist/core/credentials/caching-store.js.map +1 -0
- package/dist/core/credentials/index.d.ts +1 -0
- package/dist/core/credentials/index.d.ts.map +1 -1
- package/dist/core/credentials/index.js +1 -0
- package/dist/core/credentials/index.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +34 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils/config.d.ts +25 -0
- package/dist/core/utils/config.d.ts.map +1 -1
- package/dist/core/utils/config.js +82 -2
- 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 +71 -0
- package/dist/hermes/config-writer.d.ts.map +1 -0
- package/dist/hermes/config-writer.js +137 -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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/openclaw/integration.d.ts.map +1 -1
- package/dist/openclaw/integration.js.map +1 -1
- 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
|
|
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 │
|
|
@@ -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
|
|
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,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
|
|
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}`);
|