aquaman-proxy 0.14.1 → 0.16.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 +31 -6
- package/dist/broker-scope.d.ts +89 -0
- package/dist/broker-scope.d.ts.map +1 -0
- package/dist/broker-scope.js +186 -0
- package/dist/broker-scope.js.map +1 -0
- package/dist/cli/index.js +544 -53
- package/dist/cli/index.js.map +1 -1
- package/dist/core/credentials/backends/keeper.d.ts +69 -0
- package/dist/core/credentials/backends/keeper.d.ts.map +1 -0
- package/dist/core/credentials/backends/keeper.js +189 -0
- package/dist/core/credentials/backends/keeper.js.map +1 -0
- package/dist/core/credentials/store.d.ts +2 -0
- package/dist/core/credentials/store.d.ts.map +1 -1
- package/dist/core/credentials/store.js +7 -0
- package/dist/core/credentials/store.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/redactor/index.d.ts.map +1 -1
- package/dist/core/redactor/index.js +10 -0
- package/dist/core/redactor/index.js.map +1 -1
- package/dist/core/types.d.ts +19 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils/config.d.ts +11 -1
- package/dist/core/utils/config.d.ts.map +1 -1
- package/dist/core/utils/config.js +39 -4
- 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 +36 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +136 -15
- package/dist/daemon.js.map +1 -1
- package/dist/hermes/config-writer.d.ts +19 -0
- package/dist/hermes/config-writer.d.ts.map +1 -1
- package/dist/hermes/config-writer.js +51 -1
- package/dist/hermes/config-writer.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/migration/openclaw-migrator.d.ts.map +1 -1
- package/dist/migration/openclaw-migrator.js +7 -6
- package/dist/migration/openclaw-migrator.js.map +1 -1
- package/dist/openclaw/channel-routing.d.ts +81 -0
- package/dist/openclaw/channel-routing.d.ts.map +1 -0
- package/dist/openclaw/channel-routing.js +152 -0
- package/dist/openclaw/channel-routing.js.map +1 -0
- package/dist/openclaw/integration.d.ts +18 -0
- package/dist/openclaw/integration.d.ts.map +1 -1
- package/dist/openclaw/integration.js +32 -0
- package/dist/openclaw/integration.js.map +1 -1
- package/dist/openclaw/secretref.d.ts +35 -6
- package/dist/openclaw/secretref.d.ts.map +1 -1
- package/dist/openclaw/secretref.js +73 -8
- package/dist/openclaw/secretref.js.map +1 -1
- package/dist/request-policy.d.ts +7 -0
- package/dist/request-policy.d.ts.map +1 -1
- package/dist/request-policy.js +31 -2
- package/dist/request-policy.js.map +1 -1
- package/dist/service-registry.d.ts +8 -0
- package/dist/service-registry.d.ts.map +1 -1
- package/dist/service-registry.js +11 -4
- package/dist/service-registry.js.map +1 -1
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -12,15 +12,17 @@ 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, generateLoopbackToken, DEFAULT_LOOPBACK_PORT, wrapWithCache, resolveCacheTtl } from '../core/index.js';
|
|
15
|
+
import { loadConfig, getConfigDir, getConfigPath, 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
|
-
import { createOpenClawIntegration, authProfilesAreSqliteOnly } from '../openclaw/integration.js';
|
|
19
|
+
import { createOpenClawIntegration, authProfilesAreSqliteOnly, legacyAuthProfilesBlockProviders, pluginInstallNeedsCapabilityConsent } from '../openclaw/integration.js';
|
|
20
20
|
import { supportsSecretRefIntegrations, wireSecretRefProviders, secretRefWiringStatus } from '../openclaw/secretref.js';
|
|
21
|
+
import { CHANNEL_ROUTING_SUPPORTED, wireChannelRouting, channelRoutingStatus } from '../openclaw/channel-routing.js';
|
|
21
22
|
import { createHermesIntegration, detectHermes } from '../hermes/integration.js';
|
|
22
|
-
import { managedScopeShadowedKeys,
|
|
23
|
-
import {
|
|
23
|
+
import { managedScopeShadowedKeys, hermesManagedEnvPath, HERMES_SUPPORTED_SERVICES, hermesSecretSourceRefs } from '../hermes/config-writer.js';
|
|
24
|
+
import { createBrokerScope, parseAquamanRef, defaultProjectsPath } from '../broker-scope.js';
|
|
25
|
+
import { loadPolicyFromConfig, validatePolicyConfig, lintPolicyConfig, getDefaultPolicyPresets, matchPolicy } from '../request-policy.js';
|
|
24
26
|
import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
|
|
25
27
|
// Read version from package.json (single source of truth)
|
|
26
28
|
const __cliFilename = fileURLToPath(import.meta.url);
|
|
@@ -80,7 +82,9 @@ function storeOptionsFromConfig(config) {
|
|
|
80
82
|
keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
|
|
81
83
|
bitwardenFolder: config.credentials.bitwardenFolder,
|
|
82
84
|
bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
|
|
83
|
-
bitwardenCollectionId: config.credentials.bitwardenCollectionId
|
|
85
|
+
bitwardenCollectionId: config.credentials.bitwardenCollectionId,
|
|
86
|
+
keeperFolderUid: config.credentials.keeperFolderUid,
|
|
87
|
+
keeperConfigPath: config.credentials.keeperConfigPath
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
/**
|
|
@@ -101,6 +105,112 @@ function loadLoopbackOptions(config) {
|
|
|
101
105
|
}
|
|
102
106
|
return { port: lb.port || DEFAULT_LOOPBACK_PORT, token: lb.token, host: lb.host || '127.0.0.1' };
|
|
103
107
|
}
|
|
108
|
+
/** Broker scope for `aquaman daemon`, or undefined when `broker.enabled: false`. */
|
|
109
|
+
function daemonBrokerScope(config) {
|
|
110
|
+
if (config.broker?.enabled === false)
|
|
111
|
+
return undefined;
|
|
112
|
+
return createBrokerScope({
|
|
113
|
+
configPath: getConfigPath(),
|
|
114
|
+
loopbackDeniedServices: HERMES_SUPPORTED_SERVICES,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Add or remove refs in config.yaml `broker.allowedRefs`. Edits the raw file
|
|
119
|
+
* (not loadConfig + saveConfig) so env overrides such as the loopback token
|
|
120
|
+
* are never persisted as a side effect. The daemon re-reads the list on change.
|
|
121
|
+
*/
|
|
122
|
+
function updateBrokerAllowedRefs(refs, mode) {
|
|
123
|
+
const invalid = refs.filter(r => !parseAquamanRef(r));
|
|
124
|
+
if (invalid.length > 0) {
|
|
125
|
+
console.error(`Invalid ref${invalid.length > 1 ? 's' : ''}: ${invalid.join(', ')}`);
|
|
126
|
+
console.error('Refs look like aquaman://<service>/<key>, e.g. aquaman://github/token');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
ensureConfigDir();
|
|
130
|
+
const configPath = getConfigPath();
|
|
131
|
+
let raw = {};
|
|
132
|
+
if (fs.existsSync(configPath)) {
|
|
133
|
+
const parsed = yamlParse(fs.readFileSync(configPath, 'utf-8'));
|
|
134
|
+
if (parsed && typeof parsed === 'object')
|
|
135
|
+
raw = parsed;
|
|
136
|
+
}
|
|
137
|
+
const current = new Set(Array.isArray(raw.broker?.allowedRefs) ? raw.broker.allowedRefs : []);
|
|
138
|
+
for (const ref of refs) {
|
|
139
|
+
if (mode === 'allow')
|
|
140
|
+
current.add(ref);
|
|
141
|
+
else
|
|
142
|
+
current.delete(ref);
|
|
143
|
+
}
|
|
144
|
+
raw.broker = { ...(raw.broker && typeof raw.broker === 'object' ? raw.broker : {}), allowedRefs: [...current].sort() };
|
|
145
|
+
fs.writeFileSync(configPath, yamlStringify(raw), { encoding: 'utf-8', mode: 0o600 });
|
|
146
|
+
const verb = mode === 'allow' ? 'Allowed' : 'Revoked';
|
|
147
|
+
for (const ref of refs)
|
|
148
|
+
console.log(`${verb}: ${ref}`);
|
|
149
|
+
if (mode === 'allow') {
|
|
150
|
+
console.log('\nAny process running as you can now fetch these values from `aquaman daemon`. Only allow refs you intend to materialize.');
|
|
151
|
+
}
|
|
152
|
+
if (raw.broker.enabled === false) {
|
|
153
|
+
console.log('Note: broker.enabled is false in config.yaml, so the broker is off regardless.');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `POST /broker/resolve` against the running daemon over UDS. Always goes
|
|
158
|
+
* through the daemon (never the vault directly), so `aquaman get` is
|
|
159
|
+
* scope-checked and audited exactly like a coding-agent or Hermes resolve.
|
|
160
|
+
*/
|
|
161
|
+
function brokerResolve(socketPath, service, key, timeoutMs) {
|
|
162
|
+
const body = JSON.stringify({ service, key });
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const req = http.request({
|
|
165
|
+
socketPath,
|
|
166
|
+
path: '/broker/resolve',
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
169
|
+
timeout: timeoutMs,
|
|
170
|
+
}, (res) => {
|
|
171
|
+
let payload = '';
|
|
172
|
+
res.on('data', (chunk) => { payload += chunk; });
|
|
173
|
+
res.on('end', () => {
|
|
174
|
+
let json;
|
|
175
|
+
try {
|
|
176
|
+
json = JSON.parse(payload);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
resolve({ error: `Proxy returned a non-JSON response (HTTP ${res.statusCode})` });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (res.statusCode === 200 && typeof json.value === 'string') {
|
|
183
|
+
resolve({ value: json.value });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
resolve({ error: json.error || `Broker error (HTTP ${res.statusCode})`, fix: json.fix, code: json.code });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
req.on('error', (err) => {
|
|
191
|
+
if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
|
|
192
|
+
resolve({ error: `Cannot reach the aquaman proxy at ${socketPath}`, fix: 'Start it with: aquaman daemon', code: 'proxy_unreachable' });
|
|
193
|
+
}
|
|
194
|
+
else if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
195
|
+
resolve({ error: `Connecting to ${socketPath} was denied (${err.code})`, fix: 'If this runs inside a sandbox, allowlist the socket; otherwise check its owner and 0600 mode', code: 'socket_blocked' });
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
resolve({ error: err.message });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
req.on('timeout', () => {
|
|
202
|
+
req.destroy();
|
|
203
|
+
resolve({ error: `Broker request timed out after ${timeoutMs}ms`, fix: 'A vault that prompts (e.g. 1Password biometrics) may need longer: pass --timeout <ms>' });
|
|
204
|
+
});
|
|
205
|
+
req.end(body);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function describeBroker(scope) {
|
|
209
|
+
if (!scope)
|
|
210
|
+
return 'disabled (broker.enabled: false)';
|
|
211
|
+
const n = scope.declared().length;
|
|
212
|
+
return `${n} declared ref${n === 1 ? '' : 's'} (see: aquaman broker list)`;
|
|
213
|
+
}
|
|
104
214
|
/**
|
|
105
215
|
* Probe the loopback listener's /_health endpoint (no token required). Returns
|
|
106
216
|
* true if it responds 200 within a short timeout, false otherwise.
|
|
@@ -183,6 +293,23 @@ async function promptSecretInput(prompt) {
|
|
|
183
293
|
});
|
|
184
294
|
});
|
|
185
295
|
}
|
|
296
|
+
/** One-line explanation of why `openclaw setup` left a channel alone. */
|
|
297
|
+
function describeChannelSkip(reason, channel) {
|
|
298
|
+
switch (reason) {
|
|
299
|
+
case 'no-vault-credential':
|
|
300
|
+
return `no credential in the vault (add it: aquaman credentials add ${channel} bot_token)`;
|
|
301
|
+
case 'user-endpoint':
|
|
302
|
+
return 'it already points at your own endpoint (self-hosted Bot API or regional proxy)';
|
|
303
|
+
case 'token-file':
|
|
304
|
+
return 'its token comes from tokenFile, whose precedence over a config token we will not guess at';
|
|
305
|
+
case 'multi-account':
|
|
306
|
+
return 'multi-account channels need one vault entry per account, which the service model has no room for yet';
|
|
307
|
+
case 'not-wired':
|
|
308
|
+
return 'routable but not wired yet (re-run: aquaman openclaw setup)';
|
|
309
|
+
case 'unroutable':
|
|
310
|
+
return 'this OpenClaw exposes no endpoint override for it, so its traffic cannot be routed';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
186
313
|
const program = new Command();
|
|
187
314
|
program
|
|
188
315
|
.name('aquaman')
|
|
@@ -239,7 +366,7 @@ program
|
|
|
239
366
|
}
|
|
240
367
|
lines.push('');
|
|
241
368
|
lines.push(aqua('Vault management'));
|
|
242
|
-
for (const name of ['credentials', 'audit', 'services', 'policy']) {
|
|
369
|
+
for (const name of ['credentials', 'get', 'broker', 'audit', 'services', 'policy']) {
|
|
243
370
|
const c = byName.get(name);
|
|
244
371
|
if (c)
|
|
245
372
|
lines.push(renderCmd(c));
|
|
@@ -263,13 +390,12 @@ program
|
|
|
263
390
|
}
|
|
264
391
|
// --- Coder namespace (shim \u2014 list documented subcommands) ---
|
|
265
392
|
if (byName.has('coder')) {
|
|
266
|
-
lines.push(aqua('AI coding-agent integration (Claude Code
|
|
393
|
+
lines.push(aqua('AI coding-agent integration (Claude Code, Codex)'));
|
|
267
394
|
const coderSubs = [
|
|
268
|
-
['coder setup <agent>', 'Install hooks for an agent (claude-code
|
|
395
|
+
['coder setup <agent>', 'Install hooks for an agent (claude-code, codex)'],
|
|
269
396
|
['coder doctor', 'Deep diagnostic \u2014 projects, broker, per-project vault'],
|
|
270
397
|
['coder status', 'Configured projects + hook wiring + broker activity'],
|
|
271
398
|
['coder project list/add/remove', 'Manage ~/.aquaman/projects.yaml'],
|
|
272
|
-
['coder get <ref>', 'Resolve an aquaman://service/key reference'],
|
|
273
399
|
['coder exec <cmd>', 'Run command with project env injected + output redacted'],
|
|
274
400
|
];
|
|
275
401
|
for (const [term, desc] of coderSubs) {
|
|
@@ -316,7 +442,7 @@ program
|
|
|
316
442
|
program
|
|
317
443
|
.command('setup')
|
|
318
444
|
.description('Vault-only setup wizard \u2014 backend + credentials (use `aquaman openclaw setup` or `aquaman coder setup` for full bundles)')
|
|
319
|
-
.option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
|
|
445
|
+
.option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
|
|
320
446
|
.option('--no-policy', 'Skip request policy preset configuration')
|
|
321
447
|
.option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
|
|
322
448
|
.action(async (options) => {
|
|
@@ -541,7 +667,7 @@ async function runVaultSetup(opts) {
|
|
|
541
667
|
}
|
|
542
668
|
}
|
|
543
669
|
}
|
|
544
|
-
const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
|
|
670
|
+
const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden', 'keeper'];
|
|
545
671
|
if (!validBackends.includes(backend)) {
|
|
546
672
|
console.error(` Invalid backend: ${backend}. Valid: ${validBackends.join(', ')}`);
|
|
547
673
|
process.exit(1);
|
|
@@ -557,6 +683,9 @@ async function runVaultSetup(opts) {
|
|
|
557
683
|
config = loadConfig();
|
|
558
684
|
}
|
|
559
685
|
config.credentials.backend = backend;
|
|
686
|
+
if (backend === 'keeper' && process.env['AQUAMAN_KEEPER_FOLDER_UID']) {
|
|
687
|
+
config.credentials.keeperFolderUid = process.env['AQUAMAN_KEEPER_FOLDER_UID'];
|
|
688
|
+
}
|
|
560
689
|
fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
|
|
561
690
|
const auditDir = path.join(configDir, 'audit');
|
|
562
691
|
fs.mkdirSync(auditDir, { recursive: true, mode: 0o700 });
|
|
@@ -700,6 +829,8 @@ openclaw
|
|
|
700
829
|
// Start credential proxy
|
|
701
830
|
const socketPath = path.join(getConfigDir(), 'proxy.sock');
|
|
702
831
|
const policyConfig = loadPolicyFromConfig(config);
|
|
832
|
+
// No `broker`: this proxy fronts OpenClaw, and the OpenClaw path never
|
|
833
|
+
// hands credential values out (v0.15.0; see broker-scope.ts).
|
|
703
834
|
const credentialProxy = createCredentialProxy({
|
|
704
835
|
socketPath,
|
|
705
836
|
store: credentialStore,
|
|
@@ -710,7 +841,8 @@ openclaw
|
|
|
710
841
|
onRequest: (info) => {
|
|
711
842
|
auditLogger.logCredentialAccess('system', 'system', {
|
|
712
843
|
service: info.service,
|
|
713
|
-
|
|
844
|
+
// A broker resolve hands the value out; everything else injects it.
|
|
845
|
+
operation: info.method === 'BROKER' ? 'read' : 'use',
|
|
714
846
|
success: !info.error,
|
|
715
847
|
error: info.error
|
|
716
848
|
});
|
|
@@ -841,6 +973,10 @@ program
|
|
|
841
973
|
const serviceRegistry = createServiceRegistry({ configPath: config.services.configPath });
|
|
842
974
|
// Start credential proxy
|
|
843
975
|
const policyConfig2 = loadPolicyFromConfig(config);
|
|
976
|
+
// The daemon is the only proxy that serves the credential broker, and only
|
|
977
|
+
// for declared refs (projects.yaml + broker.allowedRefs). Over loopback the
|
|
978
|
+
// Hermes LLM-provider keys are never materialized.
|
|
979
|
+
const brokerScope = daemonBrokerScope(config);
|
|
844
980
|
const credentialProxy = createCredentialProxy({
|
|
845
981
|
socketPath,
|
|
846
982
|
store: credentialStore,
|
|
@@ -848,10 +984,12 @@ program
|
|
|
848
984
|
serviceRegistry,
|
|
849
985
|
policyConfig: policyConfig2,
|
|
850
986
|
loopback: loadLoopbackOptions(config),
|
|
987
|
+
broker: brokerScope,
|
|
851
988
|
onRequest: (info) => {
|
|
852
989
|
auditLogger.logCredentialAccess('system', 'system', {
|
|
853
990
|
service: info.service,
|
|
854
|
-
|
|
991
|
+
// A broker resolve hands the value out; everything else injects it.
|
|
992
|
+
operation: info.method === 'BROKER' ? 'read' : 'use',
|
|
855
993
|
success: !info.error,
|
|
856
994
|
error: info.error
|
|
857
995
|
});
|
|
@@ -861,6 +999,10 @@ program
|
|
|
861
999
|
// Write PID file
|
|
862
1000
|
writePidFile();
|
|
863
1001
|
console.log(`Credential proxy: ${socketPath}`);
|
|
1002
|
+
console.log(`Credential broker: ${describeBroker(brokerScope)}`);
|
|
1003
|
+
if (brokerScope?.declarationError()) {
|
|
1004
|
+
console.error(` ⚠ ${brokerScope.declarationError()} — refs declared there are refused until it parses`);
|
|
1005
|
+
}
|
|
864
1006
|
console.log(`Audit logging: ${config.audit.enabled ? 'enabled' : 'disabled'}`);
|
|
865
1007
|
console.log(`Credential backend: ${config.credentials.backend}`);
|
|
866
1008
|
console.log(`PID file: ${getPidFile()}`);
|
|
@@ -907,6 +1049,9 @@ openclaw
|
|
|
907
1049
|
const serviceRegistry = createServiceRegistry({ configPath: config.services.configPath });
|
|
908
1050
|
// Start credential proxy
|
|
909
1051
|
const policyConfig3 = loadPolicyFromConfig(config);
|
|
1052
|
+
// No `broker`: this proxy runs inside the OpenClaw plugin's lifecycle and
|
|
1053
|
+
// must never hand credential values to the gateway or its agents — that
|
|
1054
|
+
// was the ClawScan finding against 0.14.x (v0.15.0; see broker-scope.ts).
|
|
910
1055
|
const credentialProxy = createCredentialProxy({
|
|
911
1056
|
socketPath,
|
|
912
1057
|
store: credentialStore,
|
|
@@ -917,7 +1062,8 @@ openclaw
|
|
|
917
1062
|
onRequest: (info) => {
|
|
918
1063
|
auditLogger.logCredentialAccess('system', 'system', {
|
|
919
1064
|
service: info.service,
|
|
920
|
-
|
|
1065
|
+
// A broker resolve hands the value out; everything else injects it.
|
|
1066
|
+
operation: info.method === 'BROKER' ? 'read' : 'use',
|
|
921
1067
|
success: !info.error,
|
|
922
1068
|
error: info.error
|
|
923
1069
|
});
|
|
@@ -1086,7 +1232,7 @@ hermes
|
|
|
1086
1232
|
console.log(` ~/.hermes/.env: ${envWired ? aqua('wired') : 'not wired'} (${envPath})`);
|
|
1087
1233
|
const shadowed = managedScopeShadowedKeys(integration.configureHermes());
|
|
1088
1234
|
if (shadowed.length > 0) {
|
|
1089
|
-
console.log(` Managed scope: ⚠ ${
|
|
1235
|
+
console.log(` Managed scope: ⚠ ${hermesManagedEnvPath()} pins ${shadowed.join(', ')} — proxy bypassed for these keys`);
|
|
1090
1236
|
}
|
|
1091
1237
|
const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
|
|
1092
1238
|
console.log(` Hermes CLI: ${info.installed ? aqua(`v${info.version}`) : 'not found'}`);
|
|
@@ -1150,7 +1296,7 @@ hermes
|
|
|
1150
1296
|
{
|
|
1151
1297
|
const shadowed = managedScopeShadowedKeys(integration.configureHermes());
|
|
1152
1298
|
if (shadowed.length > 0) {
|
|
1153
|
-
fail(`Hermes managed scope (${
|
|
1299
|
+
fail(`Hermes managed scope (${hermesManagedEnvPath()}) 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.`);
|
|
1154
1300
|
}
|
|
1155
1301
|
else {
|
|
1156
1302
|
pass('No managed-scope override of aquaman env vars');
|
|
@@ -1177,7 +1323,38 @@ hermes
|
|
|
1177
1323
|
fail(`Vault backend "${config.credentials.backend}" failed: ${err instanceof Error ? err.message : err}`);
|
|
1178
1324
|
}
|
|
1179
1325
|
}
|
|
1180
|
-
// 5.
|
|
1326
|
+
// 5. Secret-source bindings are declared to the broker (v0.15.0+). The
|
|
1327
|
+
// daemon only materializes declared refs, so a binding in Hermes'
|
|
1328
|
+
// config.yaml alone now resolves to a "not declared" warning at startup.
|
|
1329
|
+
{
|
|
1330
|
+
const bound = hermesSecretSourceRefs();
|
|
1331
|
+
if (bound.error) {
|
|
1332
|
+
fail(`Hermes config unreadable (${bound.error}) — can't check secret-source bindings`);
|
|
1333
|
+
}
|
|
1334
|
+
else if (bound.refs.length === 0) {
|
|
1335
|
+
console.log(` • No aquaman secret-source bindings in ${bound.path} (optional)`);
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
const scope = daemonBrokerScope(config);
|
|
1339
|
+
if (!scope) {
|
|
1340
|
+
fail(`${bound.refs.length} secret-source binding(s) in ${bound.path}, but broker.enabled is false — none will resolve`);
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
const undeclared = bound.refs.filter((ref) => {
|
|
1344
|
+
const r = parseAquamanRef(ref);
|
|
1345
|
+
return !scope.check(r.service, r.key, 'loopback').allowed;
|
|
1346
|
+
});
|
|
1347
|
+
if (undeclared.length === 0) {
|
|
1348
|
+
pass(`All ${bound.refs.length} secret-source binding(s) are declared to the broker`);
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
fail(`Secret-source binding(s) not declared to the broker, so the daemon refuses them: ${undeclared.join(', ')}`);
|
|
1352
|
+
console.log(` → aquaman broker allow ${undeclared.join(' ')}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// 6. Hermes installed
|
|
1181
1358
|
const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
|
|
1182
1359
|
if (info.installed)
|
|
1183
1360
|
pass(`Hermes CLI installed (v${info.version})`);
|
|
@@ -1264,7 +1441,7 @@ program
|
|
|
1264
1441
|
openclaw
|
|
1265
1442
|
.command('setup')
|
|
1266
1443
|
.description('Full setup for OpenClaw — vault + plugin + auth profiles + (optional) auto-migrate')
|
|
1267
|
-
.option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
|
|
1444
|
+
.option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
|
|
1268
1445
|
.option('--no-openclaw', 'Skip OpenClaw plugin installation step (run vault setup only)')
|
|
1269
1446
|
.option('--no-policy', 'Skip request policy preset configuration')
|
|
1270
1447
|
.option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
|
|
@@ -1311,7 +1488,7 @@ openclaw
|
|
|
1311
1488
|
}
|
|
1312
1489
|
}
|
|
1313
1490
|
// Validate backend
|
|
1314
|
-
const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
|
|
1491
|
+
const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden', 'keeper'];
|
|
1315
1492
|
if (!validBackends.includes(backend)) {
|
|
1316
1493
|
console.error(` Invalid backend: ${backend}`);
|
|
1317
1494
|
console.error(` Valid options: ${validBackends.join(', ')}`);
|
|
@@ -1429,10 +1606,30 @@ openclaw
|
|
|
1429
1606
|
process.exit(1);
|
|
1430
1607
|
}
|
|
1431
1608
|
}
|
|
1609
|
+
else if (backend === 'keeper') {
|
|
1610
|
+
const { KeeperStore } = await import('../core/credentials/backends/keeper.js');
|
|
1611
|
+
if (!KeeperStore.isAvailable()) {
|
|
1612
|
+
console.error(' Keeper Commander (keeper) not found.');
|
|
1613
|
+
console.error(' Install: pip install keepercommander');
|
|
1614
|
+
console.error(' Then, once: keeper shell -> login -> this-device register -> this-device persistent-login on');
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
}
|
|
1617
|
+
if (!process.env['AQUAMAN_KEEPER_FOLDER_UID']) {
|
|
1618
|
+
console.error(' Keeper backend needs the folder that will hold aquaman records.');
|
|
1619
|
+
console.error(' Create one in Keeper, then: export AQUAMAN_KEEPER_FOLDER_UID=<folder UID>');
|
|
1620
|
+
console.error(' (find the UID with: keeper get "<folder name>" --format json)');
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1432
1624
|
// 2. Run init internally (create dirs, config)
|
|
1433
1625
|
ensureConfigDir();
|
|
1434
1626
|
const config = getDefaultConfig();
|
|
1435
1627
|
config.credentials.backend = backend;
|
|
1628
|
+
if (backend === 'keeper') {
|
|
1629
|
+
config.credentials.keeperFolderUid = process.env['AQUAMAN_KEEPER_FOLDER_UID'];
|
|
1630
|
+
if (process.env['AQUAMAN_KEEPER_CONFIG'])
|
|
1631
|
+
config.credentials.keeperConfigPath = process.env['AQUAMAN_KEEPER_CONFIG'];
|
|
1632
|
+
}
|
|
1436
1633
|
fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
|
|
1437
1634
|
// Create audit directory
|
|
1438
1635
|
const auditDir = path.join(configDir, 'audit');
|
|
@@ -1587,6 +1784,19 @@ openclaw
|
|
|
1587
1784
|
shouldInstall = answer.toLowerCase() !== 'n';
|
|
1588
1785
|
}
|
|
1589
1786
|
if (shouldInstall) {
|
|
1787
|
+
// Detect the gateway version once — it gates the install command
|
|
1788
|
+
// (capability consent on 2026.8.1+) and the credential surface:
|
|
1789
|
+
// SecretRef wiring (canonical, >= 2026.6.5) vs the legacy
|
|
1790
|
+
// auth-profiles.json placeholder. AQUAMAN_OPENCLAW_VERSION overrides
|
|
1791
|
+
// detection (tests + operators pinning behavior explicitly).
|
|
1792
|
+
let ocVersion = process.env.AQUAMAN_OPENCLAW_VERSION || null;
|
|
1793
|
+
if (!ocVersion) {
|
|
1794
|
+
try {
|
|
1795
|
+
const { execSync } = await import('node:child_process');
|
|
1796
|
+
ocVersion = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
|
|
1797
|
+
}
|
|
1798
|
+
catch { /* CLI not on PATH */ }
|
|
1799
|
+
}
|
|
1590
1800
|
// a. Copy plugin files
|
|
1591
1801
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1592
1802
|
const pluginSrc = path.resolve(currentDir, '../../../plugin');
|
|
@@ -1597,14 +1807,26 @@ openclaw
|
|
|
1597
1807
|
console.log(' \u2713 Plugin installed to ' + pluginDest);
|
|
1598
1808
|
}
|
|
1599
1809
|
else if (cliDetected) {
|
|
1600
|
-
// npm install — plugin source not bundled, use openclaw's plugin
|
|
1810
|
+
// npm install — plugin source not bundled, use openclaw's plugin
|
|
1811
|
+
// installer. OpenClaw 2026.8.1+ requires capability consent for
|
|
1812
|
+
// third-party plugins: interactively we let OpenClaw show its own
|
|
1813
|
+
// consent screen to the human; --non-interactive means the operator
|
|
1814
|
+
// pre-approved, so we pass --accept-capabilities and say so. The
|
|
1815
|
+
// clawhub: source avoids the extra --force an npm spec needs.
|
|
1816
|
+
const consent = pluginInstallNeedsCapabilityConsent(ocVersion);
|
|
1817
|
+
const args = consent
|
|
1818
|
+
? ['plugins', 'install', 'clawhub:aquaman-plugin', ...(isNonInteractive ? ['--accept-capabilities'] : [])]
|
|
1819
|
+
: ['plugins', 'install', 'aquaman-plugin'];
|
|
1601
1820
|
try {
|
|
1602
|
-
const {
|
|
1603
|
-
|
|
1821
|
+
const { execFileSync: execFileSyncFallback } = await import('node:child_process');
|
|
1822
|
+
execFileSyncFallback('openclaw', args, { stdio: consent && !isNonInteractive ? 'inherit' : 'pipe' });
|
|
1604
1823
|
console.log(' \u2713 Plugin installed via openclaw');
|
|
1824
|
+
if (consent && isNonInteractive) {
|
|
1825
|
+
console.log(' \u2192 Accepted the plugin\u2019s OpenClaw capabilities on your behalf (--non-interactive)');
|
|
1826
|
+
}
|
|
1605
1827
|
}
|
|
1606
1828
|
catch {
|
|
1607
|
-
console.log(
|
|
1829
|
+
console.log(` \u2717 Could not install plugin. Run: openclaw ${args.join(' ')}${consent && isNonInteractive ? '' : consent ? ' (then approve its capabilities)' : ''}`);
|
|
1608
1830
|
}
|
|
1609
1831
|
}
|
|
1610
1832
|
// b. Write/merge openclaw.json
|
|
@@ -1620,11 +1842,20 @@ openclaw
|
|
|
1620
1842
|
openclawConfig.plugins = {};
|
|
1621
1843
|
if (!openclawConfig.plugins.entries)
|
|
1622
1844
|
openclawConfig.plugins.entries = {};
|
|
1623
|
-
//
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1845
|
+
// plugins.allow is an allowlist for EVERY plugin, OpenClaw's own
|
|
1846
|
+
// included: on current gateways the anthropic/openai model providers
|
|
1847
|
+
// are stock plugins. Creating a list that holds only aquaman-plugin
|
|
1848
|
+
// (what setup did through v0.14.x) blocked them, so every
|
|
1849
|
+
// anthropic/* model became "Unknown model" (verified 2026-09-18 on
|
|
1850
|
+
// 2026.7.33 and 2026.9.1). Append to an existing list; never create
|
|
1851
|
+
// a restrictive one on the user's behalf.
|
|
1852
|
+
if (Array.isArray(openclawConfig.plugins.allow)) {
|
|
1853
|
+
if (!openclawConfig.plugins.allow.includes('aquaman-plugin')) {
|
|
1854
|
+
openclawConfig.plugins.allow.push('aquaman-plugin');
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
console.log(' \u2192 No plugins.allow list in openclaw.json; left it unset (a list with only aquaman-plugin would block OpenClaw\u2019s own provider plugins)');
|
|
1628
1859
|
}
|
|
1629
1860
|
const configuredServices = storedServices.length > 0 ? storedServices : ['anthropic', 'openai'];
|
|
1630
1861
|
openclawConfig.plugins.entries['aquaman-plugin'] = {
|
|
@@ -1634,33 +1865,91 @@ openclaw
|
|
|
1634
1865
|
services: configuredServices,
|
|
1635
1866
|
}
|
|
1636
1867
|
};
|
|
1637
|
-
// Detect the gateway version once \u2014 it gates the credential surface:
|
|
1638
|
-
// SecretRef wiring (canonical, >= 2026.6.5) vs the legacy
|
|
1639
|
-
// auth-profiles.json placeholder. AQUAMAN_OPENCLAW_VERSION overrides
|
|
1640
|
-
// detection (tests + operators pinning behavior explicitly).
|
|
1641
|
-
let ocVersion = process.env.AQUAMAN_OPENCLAW_VERSION || null;
|
|
1642
|
-
if (!ocVersion) {
|
|
1643
|
-
try {
|
|
1644
|
-
const { execSync } = await import('node:child_process');
|
|
1645
|
-
ocVersion = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
|
|
1646
|
-
}
|
|
1647
|
-
catch { /* CLI not on PATH */ }
|
|
1648
|
-
}
|
|
1649
1868
|
// b2. SecretRef provider wiring (v0.14.0+, OpenClaw >= 2026.6.5).
|
|
1650
1869
|
// Config-level refs in openclaw.json are runtime-read on every
|
|
1651
1870
|
// version and survive OpenClaw's plaintext scrubs \u2014 no SQLite
|
|
1652
1871
|
// import step needed, unlike the legacy placeholder.
|
|
1653
1872
|
const secretRefSupported = supportsSecretRefIntegrations(ocVersion);
|
|
1654
|
-
|
|
1655
|
-
|
|
1873
|
+
// Which configured channels aquaman can actually take over: the host
|
|
1874
|
+
// must expose an endpoint override and the vault must already hold
|
|
1875
|
+
// the real token. Resolved up front because the wiring is sync, and
|
|
1876
|
+
// only for channels the user actually configured -- a vault probe
|
|
1877
|
+
// can be slow (keepassxc opens and derives a key per call).
|
|
1878
|
+
const channelsConfigured = CHANNEL_ROUTING_SUPPORTED.filter(c => openclawConfig.channels?.[c] && typeof openclawConfig.channels[c] === 'object');
|
|
1879
|
+
const channelsWithVaultCredential = new Set();
|
|
1880
|
+
if (channelsConfigured.length > 0) {
|
|
1881
|
+
const channelRegistry = createServiceRegistry();
|
|
1882
|
+
for (const channel of channelsConfigured) {
|
|
1883
|
+
const def = channelRegistry.get(channel);
|
|
1884
|
+
if (!def)
|
|
1885
|
+
continue;
|
|
1886
|
+
if (await store.exists(channel, def.credentialKey)) {
|
|
1887
|
+
channelsWithVaultCredential.add(channel);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
// Both routes need the loopback listener: model traffic (provider
|
|
1892
|
+
// baseUrl) and channel egress (apiRoot). Decide once.
|
|
1893
|
+
let loopbackOrigin;
|
|
1894
|
+
let loopbackToken;
|
|
1895
|
+
if (secretRefSupported || channelsConfigured.length > 0) {
|
|
1896
|
+
// OpenClaw's model transport builds its own undici dispatcher: it
|
|
1897
|
+
// ignores the aquaman.local sentinel (its own DNS fails) and never
|
|
1898
|
+
// calls globalThis.fetch, so the plugin's interceptor can't see
|
|
1899
|
+
// provider traffic. Route it at the loopback listener instead —
|
|
1900
|
+
// OpenClaw's documented local-provider pattern (v0.15.0).
|
|
1901
|
+
const lbHost = config.loopback?.host || '127.0.0.1';
|
|
1902
|
+
const lbPort = config.loopback?.port || DEFAULT_LOOPBACK_PORT;
|
|
1903
|
+
const lbToken = config.loopback?.token || generateLoopbackToken();
|
|
1904
|
+
config.loopback = { enabled: true, host: lbHost, port: lbPort, token: lbToken };
|
|
1905
|
+
saveConfig(config);
|
|
1906
|
+
loopbackOrigin = `http://${lbHost}:${lbPort}`;
|
|
1907
|
+
loopbackToken = lbToken;
|
|
1908
|
+
console.log(` \u2713 Loopback listener enabled on ${loopbackOrigin} (token-gated, loopback-bound)`);
|
|
1909
|
+
}
|
|
1910
|
+
if (secretRefSupported && loopbackOrigin) {
|
|
1911
|
+
const wiring = wireSecretRefProviders(openclawConfig, configuredServices, { loopbackOrigin });
|
|
1656
1912
|
if (wiring.wiredProviders.length > 0) {
|
|
1657
1913
|
console.log(` \u2713 SecretRef wiring for ${wiring.wiredProviders.join(', ')} (canonical credential surface)`);
|
|
1658
1914
|
}
|
|
1915
|
+
if (wiring.baseUrlProviders.length > 0) {
|
|
1916
|
+
console.log(` \u2713 Model traffic routed through the proxy for ${wiring.baseUrlProviders.join(', ')}`);
|
|
1917
|
+
}
|
|
1918
|
+
if (wiring.keptUserBaseUrl.length > 0) {
|
|
1919
|
+
console.log(` \u2192 Left existing user-set baseUrl untouched for: ${wiring.keptUserBaseUrl.join(', ')} (proxy NOT in the path for those)`);
|
|
1920
|
+
}
|
|
1659
1921
|
const userKept = wiring.skippedProviders.filter(s => s === 'anthropic' || s === 'openai');
|
|
1660
1922
|
if (userKept.length > 0) {
|
|
1661
1923
|
console.log(` \u2192 Left existing user-set apiKey untouched for: ${userKept.join(', ')}`);
|
|
1662
1924
|
}
|
|
1663
1925
|
}
|
|
1926
|
+
// b3. Channel egress routing (v0.15.0+). Channels each build their
|
|
1927
|
+
// own undici dispatcher, so the plugin's fetch interceptor never
|
|
1928
|
+
// sees them. Telegram is the one bundled channel with an endpoint
|
|
1929
|
+
// override, and the Bot API carries its token in the path, so the
|
|
1930
|
+
// placeholder written here is the loopback token itself.
|
|
1931
|
+
if (loopbackOrigin && loopbackToken) {
|
|
1932
|
+
const routing = wireChannelRouting(openclawConfig, {
|
|
1933
|
+
loopbackOrigin,
|
|
1934
|
+
loopbackToken,
|
|
1935
|
+
hasVaultCredential: (svc) => channelsWithVaultCredential.has(svc)
|
|
1936
|
+
});
|
|
1937
|
+
if (routing.routedChannels.length > 0) {
|
|
1938
|
+
// A routed channel that isn't in proxiedServices gets a 404 from
|
|
1939
|
+
// our own proxy, which is exactly the dead bot the all-or-nothing
|
|
1940
|
+
// wiring exists to avoid. Open the route we just pointed at us.
|
|
1941
|
+
const proxied = config.credentials.proxiedServices;
|
|
1942
|
+
const added = routing.routedChannels.filter(c => !proxied.includes(c));
|
|
1943
|
+
if (added.length > 0) {
|
|
1944
|
+
config.credentials.proxiedServices = [...proxied, ...added];
|
|
1945
|
+
saveConfig(config);
|
|
1946
|
+
}
|
|
1947
|
+
console.log(` \u2713 Channel traffic routed through the proxy for ${routing.routedChannels.join(', ')}`);
|
|
1948
|
+
}
|
|
1949
|
+
for (const skip of routing.skipped) {
|
|
1950
|
+
console.log(` \u2192 ${skip.channel} left as-is: ${describeChannelSkip(skip.reason, skip.channel)}`);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1664
1953
|
fs.writeFileSync(openclawJsonPath, JSON.stringify(openclawConfig, null, 2), 'utf-8');
|
|
1665
1954
|
console.log(' \u2713 Plugin config written to ' + openclawJsonPath);
|
|
1666
1955
|
// c. Generate auth-profiles.json (only if missing). With SecretRef
|
|
@@ -1669,6 +1958,12 @@ openclaw
|
|
|
1669
1958
|
const profilesPath = path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json');
|
|
1670
1959
|
if (secretRefSupported) {
|
|
1671
1960
|
console.log(' \u2192 Legacy auth-profiles.json placeholder skipped (SecretRef wiring replaces it)');
|
|
1961
|
+
if (legacyAuthProfilesBlockProviders(ocVersion) && fs.existsSync(profilesPath)) {
|
|
1962
|
+
// Left behind by aquaman-plugin <= 0.14.x. On 2026.8.1+ it locks
|
|
1963
|
+
// anthropic/openai out until OpenClaw archives it.
|
|
1964
|
+
console.log(' \u2717 A legacy auth-profiles.json from an older aquaman-plugin blocks providers on this OpenClaw');
|
|
1965
|
+
console.log(' Run once: openclaw doctor --fix (archives it; aquaman-plugin 0.15.0+ never recreates it)');
|
|
1966
|
+
}
|
|
1672
1967
|
}
|
|
1673
1968
|
else if (!fs.existsSync(profilesPath)) {
|
|
1674
1969
|
const profiles = {};
|
|
@@ -1780,6 +2075,9 @@ openclaw
|
|
|
1780
2075
|
// 2. Backend accessible
|
|
1781
2076
|
let config;
|
|
1782
2077
|
let store = null;
|
|
2078
|
+
// Services the vault holds anything for. Used by the channel-routing
|
|
2079
|
+
// check below, which runs outside the store's try block.
|
|
2080
|
+
const vaultServices = new Set();
|
|
1783
2081
|
try {
|
|
1784
2082
|
config = loadConfig();
|
|
1785
2083
|
if (config.credentials.backend === 'systemd-creds') {
|
|
@@ -1788,6 +2086,12 @@ openclaw
|
|
|
1788
2086
|
throw new Error('systemd-creds backend requires systemd >= 256 with --user support');
|
|
1789
2087
|
}
|
|
1790
2088
|
}
|
|
2089
|
+
if (config.credentials.backend === 'keeper') {
|
|
2090
|
+
const { KeeperStore } = await import('../core/credentials/backends/keeper.js');
|
|
2091
|
+
if (!KeeperStore.isAvailable()) {
|
|
2092
|
+
throw new Error('Keeper Commander (keeper) not found. Install: pip install keepercommander');
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
1791
2095
|
if (config.credentials.backend === 'bitwarden') {
|
|
1792
2096
|
const { BitwardenStore } = await import('../core/credentials/backends/bitwarden.js');
|
|
1793
2097
|
if (!BitwardenStore.isAvailable()) {
|
|
@@ -1808,10 +2112,14 @@ openclaw
|
|
|
1808
2112
|
keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
|
|
1809
2113
|
bitwardenFolder: config.credentials.bitwardenFolder,
|
|
1810
2114
|
bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
|
|
1811
|
-
bitwardenCollectionId: config.credentials.bitwardenCollectionId
|
|
2115
|
+
bitwardenCollectionId: config.credentials.bitwardenCollectionId,
|
|
2116
|
+
keeperFolderUid: config.credentials.keeperFolderUid,
|
|
2117
|
+
keeperConfigPath: config.credentials.keeperConfigPath
|
|
1812
2118
|
});
|
|
1813
2119
|
// 3. Count credentials
|
|
1814
2120
|
const creds = await store.list();
|
|
2121
|
+
for (const c of creds)
|
|
2122
|
+
vaultServices.add(c.service);
|
|
1815
2123
|
if (creds.length > 0) {
|
|
1816
2124
|
const names = creds.map(c => `${c.service}/${c.key}`).join(', ');
|
|
1817
2125
|
console.log(` \u2713 ${aqua('Backend:')} ${config.credentials.backend} (accessible)`);
|
|
@@ -1916,6 +2224,12 @@ openclaw
|
|
|
1916
2224
|
console.log(` ${svc}: ${summary}`);
|
|
1917
2225
|
}
|
|
1918
2226
|
}
|
|
2227
|
+
// Deny rules that can never fire (pre-0.15.0 preset shapes) — a
|
|
2228
|
+
// security issue, since the operator believes the endpoint is blocked.
|
|
2229
|
+
for (const warning of lintPolicyConfig(policyConfigDoc)) {
|
|
2230
|
+
console.log(` \u2717 ${aqua('Policy')} ${warning}`);
|
|
2231
|
+
issues++;
|
|
2232
|
+
}
|
|
1919
2233
|
// Warn about policies for non-proxied services
|
|
1920
2234
|
if (config.credentials.proxiedServices) {
|
|
1921
2235
|
for (const svc of Object.keys(policyConfigDoc)) {
|
|
@@ -2020,9 +2334,21 @@ openclaw
|
|
|
2020
2334
|
if (fs.existsSync(openclawJsonPath)) {
|
|
2021
2335
|
try {
|
|
2022
2336
|
const openclawConfig = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf-8'));
|
|
2023
|
-
const allowList = openclawConfig.plugins?.allow
|
|
2024
|
-
if (allowList
|
|
2337
|
+
const allowList = Array.isArray(openclawConfig.plugins?.allow) ? openclawConfig.plugins.allow : undefined;
|
|
2338
|
+
if (!allowList) {
|
|
2339
|
+
console.log(` \u2713 ${aqua('Plugin')} allowed (no plugins.allow trust list; \`openclaw security audit\` notes extensions_no_allowlist)`);
|
|
2340
|
+
}
|
|
2341
|
+
else if (allowList.includes('aquaman-plugin')) {
|
|
2025
2342
|
console.log(` \u2713 ${aqua('Plugin')} in plugins.allow trust list`);
|
|
2343
|
+
// A list that excludes OpenClaw's own provider plugins blocks
|
|
2344
|
+
// them (aquaman setup <= 0.14.x wrote exactly such a list).
|
|
2345
|
+
const svcs = openclawConfig.plugins?.entries?.['aquaman-plugin']?.config?.services ?? ['anthropic', 'openai'];
|
|
2346
|
+
const blocked = ['anthropic', 'openai'].filter(p => svcs.includes(p) && !allowList.includes(p));
|
|
2347
|
+
if (blocked.length > 0 && supportsSecretRefIntegrations(openclawVersion)) {
|
|
2348
|
+
console.log(` \u2717 ${aqua('Plugin')} plugins.allow blocks OpenClaw\u2019s own provider plugin(s): ${blocked.join(', ')} (their models report "Unknown model")`);
|
|
2349
|
+
console.log(` \u2192 Add ${blocked.map(b => `"${b}"`).join(', ')} to plugins.allow in openclaw.json (aquaman setup <= 0.14.x created a list without them)`);
|
|
2350
|
+
issues++;
|
|
2351
|
+
}
|
|
2026
2352
|
}
|
|
2027
2353
|
else {
|
|
2028
2354
|
console.log(` \u2717 ${aqua('Plugin')} not in plugins.allow trust list`);
|
|
@@ -2048,6 +2374,17 @@ openclaw
|
|
|
2048
2374
|
if (status.providerConfigured && status.missingProviders.length === 0) {
|
|
2049
2375
|
secretRefFullyWired = true;
|
|
2050
2376
|
console.log(` ✓ ${aqua('SecretRef')} wiring active (${status.wiredProviders.join(', ') || 'no providers'})`);
|
|
2377
|
+
// The apiKey ref alone doesn't put the proxy in the path: without
|
|
2378
|
+
// a loopback baseUrl OpenClaw's transport calls the upstream
|
|
2379
|
+
// directly and the placeholder fails there (v0.15.0).
|
|
2380
|
+
if (status.missingBaseUrl.length > 0) {
|
|
2381
|
+
console.log(` ✗ ${aqua('Model routing')} ${status.missingBaseUrl.join(', ')} still call the provider directly — the proxy is bypassed`);
|
|
2382
|
+
console.log(' → Run: aquaman openclaw setup (points models.providers.<svc>.baseUrl at the loopback listener)');
|
|
2383
|
+
issues++;
|
|
2384
|
+
}
|
|
2385
|
+
else if (status.baseUrlProviders.length > 0) {
|
|
2386
|
+
console.log(` ✓ ${aqua('Model routing')} through the proxy for ${status.baseUrlProviders.join(', ')}`);
|
|
2387
|
+
}
|
|
2051
2388
|
}
|
|
2052
2389
|
else if (status.providerConfigured) {
|
|
2053
2390
|
// Half-migrated: the provider block exists but some providers still
|
|
@@ -2068,6 +2405,42 @@ openclaw
|
|
|
2068
2405
|
// openclaw.json invalid — already reported above
|
|
2069
2406
|
}
|
|
2070
2407
|
}
|
|
2408
|
+
// 8.6. Channel egress routing (v0.15.0+). Channels build their own
|
|
2409
|
+
// undici dispatcher, so the plugin's fetch interceptor never sees
|
|
2410
|
+
// them. Only channels whose endpoint OpenClaw lets us override can
|
|
2411
|
+
// be routed; the rest are at-rest storage only and say so.
|
|
2412
|
+
if (fs.existsSync(openclawJsonPath)) {
|
|
2413
|
+
try {
|
|
2414
|
+
const openclawConfig = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf-8'));
|
|
2415
|
+
const routing = channelRoutingStatus(openclawConfig, {
|
|
2416
|
+
hasVaultCredential: (svc) => vaultServices.has(svc)
|
|
2417
|
+
});
|
|
2418
|
+
if (routing.routed.length > 0) {
|
|
2419
|
+
console.log(` \u2713 ${aqua('Channel routing')} through the proxy for ${routing.routed.join(', ')}`);
|
|
2420
|
+
// Routed but not proxied means our own proxy 404s the channel.
|
|
2421
|
+
const notProxied = routing.routed.filter(c => !(config?.credentials?.proxiedServices ?? []).includes(c));
|
|
2422
|
+
if (notProxied.length > 0) {
|
|
2423
|
+
console.log(` \u2717 ${aqua('Channel routing')} ${notProxied.join(', ')} routed here but missing from proxiedServices, so the proxy returns 404`);
|
|
2424
|
+
console.log(` \u2192 Add ${notProxied.join(', ')} to credentials.proxiedServices in ~/.aquaman/config.yaml (or re-run: aquaman openclaw setup)`);
|
|
2425
|
+
issues++;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
for (const skip of routing.notRouted) {
|
|
2429
|
+
console.log(` \u2717 ${aqua('Channel routing')} ${skip.channel} sends its token straight to the vendor`);
|
|
2430
|
+
console.log(` \u2192 ${describeChannelSkip(skip.reason, skip.channel)}`);
|
|
2431
|
+
issues++;
|
|
2432
|
+
}
|
|
2433
|
+
if (routing.unroutable.length > 0) {
|
|
2434
|
+
// Not a failure: nothing the user can do, and the credentials can
|
|
2435
|
+
// still live in the vault. Stated so nobody assumes coverage.
|
|
2436
|
+
console.log(` \u2192 ${aqua('Channel routing')} not available for ${routing.unroutable.join(', ')} on this OpenClaw (no endpoint override)`);
|
|
2437
|
+
console.log(' (their tokens can live in the vault, but the proxy is not in their egress path)');
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
catch {
|
|
2441
|
+
// openclaw.json invalid \u2014 already reported above
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2071
2444
|
// 9. Auth profiles. With SecretRef wiring active this surface is
|
|
2072
2445
|
// legacy/inert. Otherwise: OpenClaw >= 2026.6.5 reads provider auth
|
|
2073
2446
|
// profiles from each agent's openclaw-agent.sqlite and no longer reads
|
|
@@ -2077,7 +2450,18 @@ openclaw
|
|
|
2077
2450
|
const agentDir = path.join(openclawStateDir, 'agents', 'main', 'agent');
|
|
2078
2451
|
const profilesPath = path.join(agentDir, 'auth-profiles.json');
|
|
2079
2452
|
const sqlitePath = path.join(agentDir, 'openclaw-agent.sqlite');
|
|
2080
|
-
if (
|
|
2453
|
+
if (legacyAuthProfilesBlockProviders(openclawVersion) && fs.existsSync(profilesPath)) {
|
|
2454
|
+
// OpenClaw 2.0 line: the legacy JSON is not inert any more — it blocks
|
|
2455
|
+
// providers with AUTH_PROFILE_MIGRATION_REQUIRED (#114033). Older
|
|
2456
|
+
// aquaman-plugin versions rewrote it on every load; 0.15.0+ doesn't.
|
|
2457
|
+
console.log(` \u2717 ${aqua('Auth profiles')} legacy auth-profiles.json blocks anthropic/openai on ${openclawVersion} (AUTH_PROFILE_MIGRATION_REQUIRED)`);
|
|
2458
|
+
if (!secretRefFullyWired) {
|
|
2459
|
+
console.log(' \u2192 First: aquaman openclaw setup (wires the SecretRef provider that replaces it)');
|
|
2460
|
+
}
|
|
2461
|
+
console.log(' \u2192 Then run once: openclaw doctor --fix (archives the file; aquaman-plugin 0.15.0+ never recreates it)');
|
|
2462
|
+
issues++;
|
|
2463
|
+
}
|
|
2464
|
+
else if (secretRefFullyWired) {
|
|
2081
2465
|
console.log(` ✓ ${aqua('Auth profiles')} not needed (SecretRef wiring replaces the placeholder flow)`);
|
|
2082
2466
|
if (fs.existsSync(profilesPath)) {
|
|
2083
2467
|
console.log(' → Optional: the legacy auth-profiles.json placeholder is inert and may be deleted');
|
|
@@ -2095,8 +2479,8 @@ openclaw
|
|
|
2095
2479
|
console.log(` \u2713 ${aqua('Auth profiles')} (SQLite store present; OpenClaw \u2265 2026.6.5)`);
|
|
2096
2480
|
}
|
|
2097
2481
|
else {
|
|
2098
|
-
console.log(` \u2717 ${aqua('Auth profiles')} missing
|
|
2099
|
-
console.log(' \u2192 Run: aquaman setup
|
|
2482
|
+
console.log(` \u2717 ${aqua('Auth profiles')} missing: no credential wiring for OpenClaw ${openclawVersion}`);
|
|
2483
|
+
console.log(' \u2192 Run: aquaman openclaw setup (wires the SecretRef provider; the plugin no longer writes auth-profiles.json here)');
|
|
2100
2484
|
issues++;
|
|
2101
2485
|
}
|
|
2102
2486
|
}
|
|
@@ -2285,7 +2669,9 @@ credentials
|
|
|
2285
2669
|
keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
|
|
2286
2670
|
bitwardenFolder: config.credentials.bitwardenFolder,
|
|
2287
2671
|
bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
|
|
2288
|
-
bitwardenCollectionId: config.credentials.bitwardenCollectionId
|
|
2672
|
+
bitwardenCollectionId: config.credentials.bitwardenCollectionId,
|
|
2673
|
+
keeperFolderUid: config.credentials.keeperFolderUid,
|
|
2674
|
+
keeperConfigPath: config.credentials.keeperConfigPath
|
|
2289
2675
|
});
|
|
2290
2676
|
}
|
|
2291
2677
|
catch (error) {
|
|
@@ -2368,7 +2754,9 @@ credentials
|
|
|
2368
2754
|
keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
|
|
2369
2755
|
bitwardenFolder: config.credentials.bitwardenFolder,
|
|
2370
2756
|
bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
|
|
2371
|
-
bitwardenCollectionId: config.credentials.bitwardenCollectionId
|
|
2757
|
+
bitwardenCollectionId: config.credentials.bitwardenCollectionId,
|
|
2758
|
+
keeperFolderUid: config.credentials.keeperFolderUid,
|
|
2759
|
+
keeperConfigPath: config.credentials.keeperConfigPath
|
|
2372
2760
|
});
|
|
2373
2761
|
}
|
|
2374
2762
|
catch {
|
|
@@ -2405,7 +2793,9 @@ credentials
|
|
|
2405
2793
|
keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
|
|
2406
2794
|
bitwardenFolder: config.credentials.bitwardenFolder,
|
|
2407
2795
|
bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
|
|
2408
|
-
bitwardenCollectionId: config.credentials.bitwardenCollectionId
|
|
2796
|
+
bitwardenCollectionId: config.credentials.bitwardenCollectionId,
|
|
2797
|
+
keeperFolderUid: config.credentials.keeperFolderUid,
|
|
2798
|
+
keeperConfigPath: config.credentials.keeperConfigPath
|
|
2409
2799
|
});
|
|
2410
2800
|
}
|
|
2411
2801
|
catch {
|
|
@@ -2423,7 +2813,7 @@ credentials
|
|
|
2423
2813
|
credentials
|
|
2424
2814
|
.command('guide')
|
|
2425
2815
|
.description('Show setup commands for seeding credentials based on your backend')
|
|
2426
|
-
.option('--backend <backend>', 'Override backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
|
|
2816
|
+
.option('--backend <backend>', 'Override backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
|
|
2427
2817
|
.option('--service <name>', 'Show commands for a single service only')
|
|
2428
2818
|
.action(async (options) => {
|
|
2429
2819
|
const config = loadConfig();
|
|
@@ -2584,6 +2974,105 @@ policy
|
|
|
2584
2974
|
console.log(` \u2717 DENIED by rule: ${rule.method} ${rule.path} \u2192 ${rule.action}`);
|
|
2585
2975
|
}
|
|
2586
2976
|
});
|
|
2977
|
+
// Credential broker scope (v0.15.0+)
|
|
2978
|
+
const broker = program
|
|
2979
|
+
.command('broker')
|
|
2980
|
+
.description('Credential broker scope: which refs `aquaman daemon` may hand out');
|
|
2981
|
+
broker
|
|
2982
|
+
.command('list')
|
|
2983
|
+
.description('List the refs the daemon broker will materialize, and where each is declared')
|
|
2984
|
+
.action(() => {
|
|
2985
|
+
const config = loadConfig();
|
|
2986
|
+
console.log('Credential broker: served by `aquaman daemon` only. OpenClaw-hosted proxies never hand out credential values.\n');
|
|
2987
|
+
if (config.broker?.enabled === false) {
|
|
2988
|
+
console.log(' Disabled (broker.enabled: false in config.yaml) — every broker request is refused.');
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
const scope = daemonBrokerScope(config);
|
|
2992
|
+
const declared = scope.declared();
|
|
2993
|
+
if (declared.length === 0) {
|
|
2994
|
+
console.log(' No declared refs — every broker request is refused.');
|
|
2995
|
+
}
|
|
2996
|
+
for (const d of declared)
|
|
2997
|
+
console.log(` ${d.ref} (${d.source})`);
|
|
2998
|
+
const err = scope.declarationError();
|
|
2999
|
+
if (err)
|
|
3000
|
+
console.log(`\n ⚠ ${err} — refs declared there are refused until it parses`);
|
|
3001
|
+
console.log(`\n Never over the loopback listener: ${HERMES_SUPPORTED_SERVICES.join(', ')} (process-isolated on the proxy path)`);
|
|
3002
|
+
console.log(` Coding-agent refs: ${defaultProjectsPath()} (aquaman coder project add ...)`);
|
|
3003
|
+
console.log(` Other refs: broker.allowedRefs in ${getConfigPath()} (aquaman broker allow ...)`);
|
|
3004
|
+
});
|
|
3005
|
+
broker
|
|
3006
|
+
.command('allow')
|
|
3007
|
+
.description('Allow aquaman://service/key refs to be materialized (e.g. Hermes secret-source bindings)')
|
|
3008
|
+
.argument('<refs...>', 'one or more aquaman://service/key refs')
|
|
3009
|
+
.action((refs) => {
|
|
3010
|
+
updateBrokerAllowedRefs(refs, 'allow');
|
|
3011
|
+
});
|
|
3012
|
+
broker
|
|
3013
|
+
.command('revoke')
|
|
3014
|
+
.description('Stop materializing refs added with `aquaman broker allow`')
|
|
3015
|
+
.argument('<refs...>', 'one or more aquaman://service/key refs')
|
|
3016
|
+
.action((refs) => {
|
|
3017
|
+
updateBrokerAllowedRefs(refs, 'revoke');
|
|
3018
|
+
});
|
|
3019
|
+
// v0.16.0: fills the "run a command, read the secret from stdout" slot every
|
|
3020
|
+
// host now has (sbx secret set --command, Codex model_providers.*.auth.command,
|
|
3021
|
+
// Claude Code apiKeyHelper, OpenClaw exec providers, Hermes command sources).
|
|
3022
|
+
// This materializes the value into the CALLING process, so it is not process
|
|
3023
|
+
// isolation; it is scope-checked and audited because it goes through the daemon.
|
|
3024
|
+
program
|
|
3025
|
+
.command('get')
|
|
3026
|
+
.description('Print a declared credential to stdout, via the running daemon (for host credential-helper commands)')
|
|
3027
|
+
.argument('<ref>', 'aquaman://service/key, declared with `aquaman broker allow` or in projects.yaml')
|
|
3028
|
+
.option('--show', 'Allow printing to an interactive terminal')
|
|
3029
|
+
.option('--timeout <ms>', 'How long to wait for the daemon (vaults that prompt may need longer)', '15000')
|
|
3030
|
+
.addHelpText('after', `
|
|
3031
|
+
Examples:
|
|
3032
|
+
sbx secret set github --command "$(command -v node) $(command -v aquaman) get aquaman://github/token"
|
|
3033
|
+
Codex config.toml: [model_providers.openai.auth] command = "aquaman" args = ["get", "aquaman://openai/api_key"]
|
|
3034
|
+
Claude Code: "apiKeyHelper": "aquaman get aquaman://anthropic/api_key"
|
|
3035
|
+
|
|
3036
|
+
The value lands in whichever process runs this command. Every read is checked
|
|
3037
|
+
against the broker scope and written to the audit log.`)
|
|
3038
|
+
.action(async (ref, opts) => {
|
|
3039
|
+
const parsed = parseAquamanRef(ref);
|
|
3040
|
+
if (!parsed) {
|
|
3041
|
+
console.error(`Invalid ref: ${ref}`);
|
|
3042
|
+
console.error('Refs look like aquaman://<service>/<key>, e.g. aquaman://github/token');
|
|
3043
|
+
process.exit(1);
|
|
3044
|
+
}
|
|
3045
|
+
const timeoutMs = Number(opts.timeout);
|
|
3046
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
|
|
3047
|
+
console.error(`Invalid --timeout: ${opts.timeout} (milliseconds, a positive integer)`);
|
|
3048
|
+
process.exit(1);
|
|
3049
|
+
}
|
|
3050
|
+
// Refuse before resolving, so an accidental run at a prompt neither
|
|
3051
|
+
// prints the secret into scrollback nor spends an audited read.
|
|
3052
|
+
if (process.stdout.isTTY && !opts.show) {
|
|
3053
|
+
console.error('Refusing to print a credential to a terminal.');
|
|
3054
|
+
console.error('This command is meant to be run by another program. To print it anyway, pass --show.');
|
|
3055
|
+
process.exit(1);
|
|
3056
|
+
}
|
|
3057
|
+
const result = await brokerResolve(path.join(getConfigDir(), 'proxy.sock'), parsed.service, parsed.key, timeoutMs);
|
|
3058
|
+
if ('value' in result) {
|
|
3059
|
+
// No trailing newline when piped: credential-helper consumers differ on
|
|
3060
|
+
// whether they trim, and a stray "\n" in an auth header breaks requests.
|
|
3061
|
+
process.stdout.write(process.stdout.isTTY ? `${result.value}\n` : result.value);
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
console.error(`Error: ${result.error}`);
|
|
3065
|
+
if (result.code === 'broker_ref_not_declared') {
|
|
3066
|
+
console.error(`Fix: aquaman broker allow ${ref}`);
|
|
3067
|
+
}
|
|
3068
|
+
else if (result.code === 'broker_disabled') {
|
|
3069
|
+
console.error('Fix: the proxy on this socket does not serve the broker. If the OpenClaw plugin started it, run `aquaman daemon` instead (the last proxy started owns the socket).');
|
|
3070
|
+
}
|
|
3071
|
+
else if (result.fix) {
|
|
3072
|
+
console.error(`Fix: ${result.fix}`);
|
|
3073
|
+
}
|
|
3074
|
+
process.exit(1);
|
|
3075
|
+
});
|
|
2587
3076
|
// Migration commands
|
|
2588
3077
|
openclaw
|
|
2589
3078
|
.command('migrate')
|
|
@@ -3017,7 +3506,9 @@ openclaw
|
|
|
3017
3506
|
keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
|
|
3018
3507
|
bitwardenFolder: config.credentials.bitwardenFolder,
|
|
3019
3508
|
bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
|
|
3020
|
-
bitwardenCollectionId: config.credentials.bitwardenCollectionId
|
|
3509
|
+
bitwardenCollectionId: config.credentials.bitwardenCollectionId,
|
|
3510
|
+
keeperFolderUid: config.credentials.keeperFolderUid,
|
|
3511
|
+
keeperConfigPath: config.credentials.keeperConfigPath
|
|
3021
3512
|
});
|
|
3022
3513
|
const creds = await store.list();
|
|
3023
3514
|
console.log(`\nStored credentials: ${creds.length}`);
|