aquaman-proxy 0.14.1 → 0.15.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 +28 -5
- 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 +374 -35
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +14 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils/config.d.ts +9 -0
- package/dist/core/utils/config.d.ts.map +1 -1
- package/dist/core/utils/config.js +29 -1
- package/dist/core/utils/config.js.map +1 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/core/utils/index.js.map +1 -1
- package/dist/daemon.d.ts +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 +2 -2
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);
|
|
@@ -101,6 +103,60 @@ function loadLoopbackOptions(config) {
|
|
|
101
103
|
}
|
|
102
104
|
return { port: lb.port || DEFAULT_LOOPBACK_PORT, token: lb.token, host: lb.host || '127.0.0.1' };
|
|
103
105
|
}
|
|
106
|
+
/** Broker scope for `aquaman daemon`, or undefined when `broker.enabled: false`. */
|
|
107
|
+
function daemonBrokerScope(config) {
|
|
108
|
+
if (config.broker?.enabled === false)
|
|
109
|
+
return undefined;
|
|
110
|
+
return createBrokerScope({
|
|
111
|
+
configPath: getConfigPath(),
|
|
112
|
+
loopbackDeniedServices: HERMES_SUPPORTED_SERVICES,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Add or remove refs in config.yaml `broker.allowedRefs`. Edits the raw file
|
|
117
|
+
* (not loadConfig + saveConfig) so env overrides such as the loopback token
|
|
118
|
+
* are never persisted as a side effect. The daemon re-reads the list on change.
|
|
119
|
+
*/
|
|
120
|
+
function updateBrokerAllowedRefs(refs, mode) {
|
|
121
|
+
const invalid = refs.filter(r => !parseAquamanRef(r));
|
|
122
|
+
if (invalid.length > 0) {
|
|
123
|
+
console.error(`Invalid ref${invalid.length > 1 ? 's' : ''}: ${invalid.join(', ')}`);
|
|
124
|
+
console.error('Refs look like aquaman://<service>/<key>, e.g. aquaman://github/token');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
ensureConfigDir();
|
|
128
|
+
const configPath = getConfigPath();
|
|
129
|
+
let raw = {};
|
|
130
|
+
if (fs.existsSync(configPath)) {
|
|
131
|
+
const parsed = yamlParse(fs.readFileSync(configPath, 'utf-8'));
|
|
132
|
+
if (parsed && typeof parsed === 'object')
|
|
133
|
+
raw = parsed;
|
|
134
|
+
}
|
|
135
|
+
const current = new Set(Array.isArray(raw.broker?.allowedRefs) ? raw.broker.allowedRefs : []);
|
|
136
|
+
for (const ref of refs) {
|
|
137
|
+
if (mode === 'allow')
|
|
138
|
+
current.add(ref);
|
|
139
|
+
else
|
|
140
|
+
current.delete(ref);
|
|
141
|
+
}
|
|
142
|
+
raw.broker = { ...(raw.broker && typeof raw.broker === 'object' ? raw.broker : {}), allowedRefs: [...current].sort() };
|
|
143
|
+
fs.writeFileSync(configPath, yamlStringify(raw), { encoding: 'utf-8', mode: 0o600 });
|
|
144
|
+
const verb = mode === 'allow' ? 'Allowed' : 'Revoked';
|
|
145
|
+
for (const ref of refs)
|
|
146
|
+
console.log(`${verb}: ${ref}`);
|
|
147
|
+
if (mode === 'allow') {
|
|
148
|
+
console.log('\nAny process running as you can now fetch these values from `aquaman daemon`. Only allow refs you intend to materialize.');
|
|
149
|
+
}
|
|
150
|
+
if (raw.broker.enabled === false) {
|
|
151
|
+
console.log('Note: broker.enabled is false in config.yaml, so the broker is off regardless.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function describeBroker(scope) {
|
|
155
|
+
if (!scope)
|
|
156
|
+
return 'disabled (broker.enabled: false)';
|
|
157
|
+
const n = scope.declared().length;
|
|
158
|
+
return `${n} declared ref${n === 1 ? '' : 's'} (see: aquaman broker list)`;
|
|
159
|
+
}
|
|
104
160
|
/**
|
|
105
161
|
* Probe the loopback listener's /_health endpoint (no token required). Returns
|
|
106
162
|
* true if it responds 200 within a short timeout, false otherwise.
|
|
@@ -183,6 +239,23 @@ async function promptSecretInput(prompt) {
|
|
|
183
239
|
});
|
|
184
240
|
});
|
|
185
241
|
}
|
|
242
|
+
/** One-line explanation of why `openclaw setup` left a channel alone. */
|
|
243
|
+
function describeChannelSkip(reason, channel) {
|
|
244
|
+
switch (reason) {
|
|
245
|
+
case 'no-vault-credential':
|
|
246
|
+
return `no credential in the vault (add it: aquaman credentials add ${channel} bot_token)`;
|
|
247
|
+
case 'user-endpoint':
|
|
248
|
+
return 'it already points at your own endpoint (self-hosted Bot API or regional proxy)';
|
|
249
|
+
case 'token-file':
|
|
250
|
+
return 'its token comes from tokenFile, whose precedence over a config token we will not guess at';
|
|
251
|
+
case 'multi-account':
|
|
252
|
+
return 'multi-account channels need one vault entry per account, which the service model has no room for yet';
|
|
253
|
+
case 'not-wired':
|
|
254
|
+
return 'routable but not wired yet (re-run: aquaman openclaw setup)';
|
|
255
|
+
case 'unroutable':
|
|
256
|
+
return 'this OpenClaw exposes no endpoint override for it, so its traffic cannot be routed';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
186
259
|
const program = new Command();
|
|
187
260
|
program
|
|
188
261
|
.name('aquaman')
|
|
@@ -700,6 +773,8 @@ openclaw
|
|
|
700
773
|
// Start credential proxy
|
|
701
774
|
const socketPath = path.join(getConfigDir(), 'proxy.sock');
|
|
702
775
|
const policyConfig = loadPolicyFromConfig(config);
|
|
776
|
+
// No `broker`: this proxy fronts OpenClaw, and the OpenClaw path never
|
|
777
|
+
// hands credential values out (v0.15.0; see broker-scope.ts).
|
|
703
778
|
const credentialProxy = createCredentialProxy({
|
|
704
779
|
socketPath,
|
|
705
780
|
store: credentialStore,
|
|
@@ -841,6 +916,10 @@ program
|
|
|
841
916
|
const serviceRegistry = createServiceRegistry({ configPath: config.services.configPath });
|
|
842
917
|
// Start credential proxy
|
|
843
918
|
const policyConfig2 = loadPolicyFromConfig(config);
|
|
919
|
+
// The daemon is the only proxy that serves the credential broker, and only
|
|
920
|
+
// for declared refs (projects.yaml + broker.allowedRefs). Over loopback the
|
|
921
|
+
// Hermes LLM-provider keys are never materialized.
|
|
922
|
+
const brokerScope = daemonBrokerScope(config);
|
|
844
923
|
const credentialProxy = createCredentialProxy({
|
|
845
924
|
socketPath,
|
|
846
925
|
store: credentialStore,
|
|
@@ -848,6 +927,7 @@ program
|
|
|
848
927
|
serviceRegistry,
|
|
849
928
|
policyConfig: policyConfig2,
|
|
850
929
|
loopback: loadLoopbackOptions(config),
|
|
930
|
+
broker: brokerScope,
|
|
851
931
|
onRequest: (info) => {
|
|
852
932
|
auditLogger.logCredentialAccess('system', 'system', {
|
|
853
933
|
service: info.service,
|
|
@@ -861,6 +941,10 @@ program
|
|
|
861
941
|
// Write PID file
|
|
862
942
|
writePidFile();
|
|
863
943
|
console.log(`Credential proxy: ${socketPath}`);
|
|
944
|
+
console.log(`Credential broker: ${describeBroker(brokerScope)}`);
|
|
945
|
+
if (brokerScope?.declarationError()) {
|
|
946
|
+
console.error(` ⚠ ${brokerScope.declarationError()} — refs declared there are refused until it parses`);
|
|
947
|
+
}
|
|
864
948
|
console.log(`Audit logging: ${config.audit.enabled ? 'enabled' : 'disabled'}`);
|
|
865
949
|
console.log(`Credential backend: ${config.credentials.backend}`);
|
|
866
950
|
console.log(`PID file: ${getPidFile()}`);
|
|
@@ -907,6 +991,9 @@ openclaw
|
|
|
907
991
|
const serviceRegistry = createServiceRegistry({ configPath: config.services.configPath });
|
|
908
992
|
// Start credential proxy
|
|
909
993
|
const policyConfig3 = loadPolicyFromConfig(config);
|
|
994
|
+
// No `broker`: this proxy runs inside the OpenClaw plugin's lifecycle and
|
|
995
|
+
// must never hand credential values to the gateway or its agents — that
|
|
996
|
+
// was the ClawScan finding against 0.14.x (v0.15.0; see broker-scope.ts).
|
|
910
997
|
const credentialProxy = createCredentialProxy({
|
|
911
998
|
socketPath,
|
|
912
999
|
store: credentialStore,
|
|
@@ -1086,7 +1173,7 @@ hermes
|
|
|
1086
1173
|
console.log(` ~/.hermes/.env: ${envWired ? aqua('wired') : 'not wired'} (${envPath})`);
|
|
1087
1174
|
const shadowed = managedScopeShadowedKeys(integration.configureHermes());
|
|
1088
1175
|
if (shadowed.length > 0) {
|
|
1089
|
-
console.log(` Managed scope: ⚠ ${
|
|
1176
|
+
console.log(` Managed scope: ⚠ ${hermesManagedEnvPath()} pins ${shadowed.join(', ')} — proxy bypassed for these keys`);
|
|
1090
1177
|
}
|
|
1091
1178
|
const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
|
|
1092
1179
|
console.log(` Hermes CLI: ${info.installed ? aqua(`v${info.version}`) : 'not found'}`);
|
|
@@ -1150,7 +1237,7 @@ hermes
|
|
|
1150
1237
|
{
|
|
1151
1238
|
const shadowed = managedScopeShadowedKeys(integration.configureHermes());
|
|
1152
1239
|
if (shadowed.length > 0) {
|
|
1153
|
-
fail(`Hermes managed scope (${
|
|
1240
|
+
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
1241
|
}
|
|
1155
1242
|
else {
|
|
1156
1243
|
pass('No managed-scope override of aquaman env vars');
|
|
@@ -1177,7 +1264,38 @@ hermes
|
|
|
1177
1264
|
fail(`Vault backend "${config.credentials.backend}" failed: ${err instanceof Error ? err.message : err}`);
|
|
1178
1265
|
}
|
|
1179
1266
|
}
|
|
1180
|
-
// 5.
|
|
1267
|
+
// 5. Secret-source bindings are declared to the broker (v0.15.0+). The
|
|
1268
|
+
// daemon only materializes declared refs, so a binding in Hermes'
|
|
1269
|
+
// config.yaml alone now resolves to a "not declared" warning at startup.
|
|
1270
|
+
{
|
|
1271
|
+
const bound = hermesSecretSourceRefs();
|
|
1272
|
+
if (bound.error) {
|
|
1273
|
+
fail(`Hermes config unreadable (${bound.error}) — can't check secret-source bindings`);
|
|
1274
|
+
}
|
|
1275
|
+
else if (bound.refs.length === 0) {
|
|
1276
|
+
console.log(` • No aquaman secret-source bindings in ${bound.path} (optional)`);
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
const scope = daemonBrokerScope(config);
|
|
1280
|
+
if (!scope) {
|
|
1281
|
+
fail(`${bound.refs.length} secret-source binding(s) in ${bound.path}, but broker.enabled is false — none will resolve`);
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1284
|
+
const undeclared = bound.refs.filter((ref) => {
|
|
1285
|
+
const r = parseAquamanRef(ref);
|
|
1286
|
+
return !scope.check(r.service, r.key, 'loopback').allowed;
|
|
1287
|
+
});
|
|
1288
|
+
if (undeclared.length === 0) {
|
|
1289
|
+
pass(`All ${bound.refs.length} secret-source binding(s) are declared to the broker`);
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
fail(`Secret-source binding(s) not declared to the broker, so the daemon refuses them: ${undeclared.join(', ')}`);
|
|
1293
|
+
console.log(` → aquaman broker allow ${undeclared.join(' ')}`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// 6. Hermes installed
|
|
1181
1299
|
const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
|
|
1182
1300
|
if (info.installed)
|
|
1183
1301
|
pass(`Hermes CLI installed (v${info.version})`);
|
|
@@ -1587,6 +1705,19 @@ openclaw
|
|
|
1587
1705
|
shouldInstall = answer.toLowerCase() !== 'n';
|
|
1588
1706
|
}
|
|
1589
1707
|
if (shouldInstall) {
|
|
1708
|
+
// Detect the gateway version once — it gates the install command
|
|
1709
|
+
// (capability consent on 2026.8.1+) and the credential surface:
|
|
1710
|
+
// SecretRef wiring (canonical, >= 2026.6.5) vs the legacy
|
|
1711
|
+
// auth-profiles.json placeholder. AQUAMAN_OPENCLAW_VERSION overrides
|
|
1712
|
+
// detection (tests + operators pinning behavior explicitly).
|
|
1713
|
+
let ocVersion = process.env.AQUAMAN_OPENCLAW_VERSION || null;
|
|
1714
|
+
if (!ocVersion) {
|
|
1715
|
+
try {
|
|
1716
|
+
const { execSync } = await import('node:child_process');
|
|
1717
|
+
ocVersion = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
|
|
1718
|
+
}
|
|
1719
|
+
catch { /* CLI not on PATH */ }
|
|
1720
|
+
}
|
|
1590
1721
|
// a. Copy plugin files
|
|
1591
1722
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1592
1723
|
const pluginSrc = path.resolve(currentDir, '../../../plugin');
|
|
@@ -1597,14 +1728,26 @@ openclaw
|
|
|
1597
1728
|
console.log(' \u2713 Plugin installed to ' + pluginDest);
|
|
1598
1729
|
}
|
|
1599
1730
|
else if (cliDetected) {
|
|
1600
|
-
// npm install — plugin source not bundled, use openclaw's plugin
|
|
1731
|
+
// npm install — plugin source not bundled, use openclaw's plugin
|
|
1732
|
+
// installer. OpenClaw 2026.8.1+ requires capability consent for
|
|
1733
|
+
// third-party plugins: interactively we let OpenClaw show its own
|
|
1734
|
+
// consent screen to the human; --non-interactive means the operator
|
|
1735
|
+
// pre-approved, so we pass --accept-capabilities and say so. The
|
|
1736
|
+
// clawhub: source avoids the extra --force an npm spec needs.
|
|
1737
|
+
const consent = pluginInstallNeedsCapabilityConsent(ocVersion);
|
|
1738
|
+
const args = consent
|
|
1739
|
+
? ['plugins', 'install', 'clawhub:aquaman-plugin', ...(isNonInteractive ? ['--accept-capabilities'] : [])]
|
|
1740
|
+
: ['plugins', 'install', 'aquaman-plugin'];
|
|
1601
1741
|
try {
|
|
1602
|
-
const {
|
|
1603
|
-
|
|
1742
|
+
const { execFileSync: execFileSyncFallback } = await import('node:child_process');
|
|
1743
|
+
execFileSyncFallback('openclaw', args, { stdio: consent && !isNonInteractive ? 'inherit' : 'pipe' });
|
|
1604
1744
|
console.log(' \u2713 Plugin installed via openclaw');
|
|
1745
|
+
if (consent && isNonInteractive) {
|
|
1746
|
+
console.log(' \u2192 Accepted the plugin\u2019s OpenClaw capabilities on your behalf (--non-interactive)');
|
|
1747
|
+
}
|
|
1605
1748
|
}
|
|
1606
1749
|
catch {
|
|
1607
|
-
console.log(
|
|
1750
|
+
console.log(` \u2717 Could not install plugin. Run: openclaw ${args.join(' ')}${consent && isNonInteractive ? '' : consent ? ' (then approve its capabilities)' : ''}`);
|
|
1608
1751
|
}
|
|
1609
1752
|
}
|
|
1610
1753
|
// b. Write/merge openclaw.json
|
|
@@ -1620,11 +1763,20 @@ openclaw
|
|
|
1620
1763
|
openclawConfig.plugins = {};
|
|
1621
1764
|
if (!openclawConfig.plugins.entries)
|
|
1622
1765
|
openclawConfig.plugins.entries = {};
|
|
1623
|
-
//
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1766
|
+
// plugins.allow is an allowlist for EVERY plugin, OpenClaw's own
|
|
1767
|
+
// included: on current gateways the anthropic/openai model providers
|
|
1768
|
+
// are stock plugins. Creating a list that holds only aquaman-plugin
|
|
1769
|
+
// (what setup did through v0.14.x) blocked them, so every
|
|
1770
|
+
// anthropic/* model became "Unknown model" (verified 2026-09-18 on
|
|
1771
|
+
// 2026.7.33 and 2026.9.1). Append to an existing list; never create
|
|
1772
|
+
// a restrictive one on the user's behalf.
|
|
1773
|
+
if (Array.isArray(openclawConfig.plugins.allow)) {
|
|
1774
|
+
if (!openclawConfig.plugins.allow.includes('aquaman-plugin')) {
|
|
1775
|
+
openclawConfig.plugins.allow.push('aquaman-plugin');
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
else {
|
|
1779
|
+
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
1780
|
}
|
|
1629
1781
|
const configuredServices = storedServices.length > 0 ? storedServices : ['anthropic', 'openai'];
|
|
1630
1782
|
openclawConfig.plugins.entries['aquaman-plugin'] = {
|
|
@@ -1634,33 +1786,91 @@ openclaw
|
|
|
1634
1786
|
services: configuredServices,
|
|
1635
1787
|
}
|
|
1636
1788
|
};
|
|
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
1789
|
// b2. SecretRef provider wiring (v0.14.0+, OpenClaw >= 2026.6.5).
|
|
1650
1790
|
// Config-level refs in openclaw.json are runtime-read on every
|
|
1651
1791
|
// version and survive OpenClaw's plaintext scrubs \u2014 no SQLite
|
|
1652
1792
|
// import step needed, unlike the legacy placeholder.
|
|
1653
1793
|
const secretRefSupported = supportsSecretRefIntegrations(ocVersion);
|
|
1654
|
-
|
|
1655
|
-
|
|
1794
|
+
// Which configured channels aquaman can actually take over: the host
|
|
1795
|
+
// must expose an endpoint override and the vault must already hold
|
|
1796
|
+
// the real token. Resolved up front because the wiring is sync, and
|
|
1797
|
+
// only for channels the user actually configured -- a vault probe
|
|
1798
|
+
// can be slow (keepassxc opens and derives a key per call).
|
|
1799
|
+
const channelsConfigured = CHANNEL_ROUTING_SUPPORTED.filter(c => openclawConfig.channels?.[c] && typeof openclawConfig.channels[c] === 'object');
|
|
1800
|
+
const channelsWithVaultCredential = new Set();
|
|
1801
|
+
if (channelsConfigured.length > 0) {
|
|
1802
|
+
const channelRegistry = createServiceRegistry();
|
|
1803
|
+
for (const channel of channelsConfigured) {
|
|
1804
|
+
const def = channelRegistry.get(channel);
|
|
1805
|
+
if (!def)
|
|
1806
|
+
continue;
|
|
1807
|
+
if (await store.exists(channel, def.credentialKey)) {
|
|
1808
|
+
channelsWithVaultCredential.add(channel);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
// Both routes need the loopback listener: model traffic (provider
|
|
1813
|
+
// baseUrl) and channel egress (apiRoot). Decide once.
|
|
1814
|
+
let loopbackOrigin;
|
|
1815
|
+
let loopbackToken;
|
|
1816
|
+
if (secretRefSupported || channelsConfigured.length > 0) {
|
|
1817
|
+
// OpenClaw's model transport builds its own undici dispatcher: it
|
|
1818
|
+
// ignores the aquaman.local sentinel (its own DNS fails) and never
|
|
1819
|
+
// calls globalThis.fetch, so the plugin's interceptor can't see
|
|
1820
|
+
// provider traffic. Route it at the loopback listener instead —
|
|
1821
|
+
// OpenClaw's documented local-provider pattern (v0.15.0).
|
|
1822
|
+
const lbHost = config.loopback?.host || '127.0.0.1';
|
|
1823
|
+
const lbPort = config.loopback?.port || DEFAULT_LOOPBACK_PORT;
|
|
1824
|
+
const lbToken = config.loopback?.token || generateLoopbackToken();
|
|
1825
|
+
config.loopback = { enabled: true, host: lbHost, port: lbPort, token: lbToken };
|
|
1826
|
+
saveConfig(config);
|
|
1827
|
+
loopbackOrigin = `http://${lbHost}:${lbPort}`;
|
|
1828
|
+
loopbackToken = lbToken;
|
|
1829
|
+
console.log(` \u2713 Loopback listener enabled on ${loopbackOrigin} (token-gated, loopback-bound)`);
|
|
1830
|
+
}
|
|
1831
|
+
if (secretRefSupported && loopbackOrigin) {
|
|
1832
|
+
const wiring = wireSecretRefProviders(openclawConfig, configuredServices, { loopbackOrigin });
|
|
1656
1833
|
if (wiring.wiredProviders.length > 0) {
|
|
1657
1834
|
console.log(` \u2713 SecretRef wiring for ${wiring.wiredProviders.join(', ')} (canonical credential surface)`);
|
|
1658
1835
|
}
|
|
1836
|
+
if (wiring.baseUrlProviders.length > 0) {
|
|
1837
|
+
console.log(` \u2713 Model traffic routed through the proxy for ${wiring.baseUrlProviders.join(', ')}`);
|
|
1838
|
+
}
|
|
1839
|
+
if (wiring.keptUserBaseUrl.length > 0) {
|
|
1840
|
+
console.log(` \u2192 Left existing user-set baseUrl untouched for: ${wiring.keptUserBaseUrl.join(', ')} (proxy NOT in the path for those)`);
|
|
1841
|
+
}
|
|
1659
1842
|
const userKept = wiring.skippedProviders.filter(s => s === 'anthropic' || s === 'openai');
|
|
1660
1843
|
if (userKept.length > 0) {
|
|
1661
1844
|
console.log(` \u2192 Left existing user-set apiKey untouched for: ${userKept.join(', ')}`);
|
|
1662
1845
|
}
|
|
1663
1846
|
}
|
|
1847
|
+
// b3. Channel egress routing (v0.15.0+). Channels each build their
|
|
1848
|
+
// own undici dispatcher, so the plugin's fetch interceptor never
|
|
1849
|
+
// sees them. Telegram is the one bundled channel with an endpoint
|
|
1850
|
+
// override, and the Bot API carries its token in the path, so the
|
|
1851
|
+
// placeholder written here is the loopback token itself.
|
|
1852
|
+
if (loopbackOrigin && loopbackToken) {
|
|
1853
|
+
const routing = wireChannelRouting(openclawConfig, {
|
|
1854
|
+
loopbackOrigin,
|
|
1855
|
+
loopbackToken,
|
|
1856
|
+
hasVaultCredential: (svc) => channelsWithVaultCredential.has(svc)
|
|
1857
|
+
});
|
|
1858
|
+
if (routing.routedChannels.length > 0) {
|
|
1859
|
+
// A routed channel that isn't in proxiedServices gets a 404 from
|
|
1860
|
+
// our own proxy, which is exactly the dead bot the all-or-nothing
|
|
1861
|
+
// wiring exists to avoid. Open the route we just pointed at us.
|
|
1862
|
+
const proxied = config.credentials.proxiedServices;
|
|
1863
|
+
const added = routing.routedChannels.filter(c => !proxied.includes(c));
|
|
1864
|
+
if (added.length > 0) {
|
|
1865
|
+
config.credentials.proxiedServices = [...proxied, ...added];
|
|
1866
|
+
saveConfig(config);
|
|
1867
|
+
}
|
|
1868
|
+
console.log(` \u2713 Channel traffic routed through the proxy for ${routing.routedChannels.join(', ')}`);
|
|
1869
|
+
}
|
|
1870
|
+
for (const skip of routing.skipped) {
|
|
1871
|
+
console.log(` \u2192 ${skip.channel} left as-is: ${describeChannelSkip(skip.reason, skip.channel)}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1664
1874
|
fs.writeFileSync(openclawJsonPath, JSON.stringify(openclawConfig, null, 2), 'utf-8');
|
|
1665
1875
|
console.log(' \u2713 Plugin config written to ' + openclawJsonPath);
|
|
1666
1876
|
// c. Generate auth-profiles.json (only if missing). With SecretRef
|
|
@@ -1669,6 +1879,12 @@ openclaw
|
|
|
1669
1879
|
const profilesPath = path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json');
|
|
1670
1880
|
if (secretRefSupported) {
|
|
1671
1881
|
console.log(' \u2192 Legacy auth-profiles.json placeholder skipped (SecretRef wiring replaces it)');
|
|
1882
|
+
if (legacyAuthProfilesBlockProviders(ocVersion) && fs.existsSync(profilesPath)) {
|
|
1883
|
+
// Left behind by aquaman-plugin <= 0.14.x. On 2026.8.1+ it locks
|
|
1884
|
+
// anthropic/openai out until OpenClaw archives it.
|
|
1885
|
+
console.log(' \u2717 A legacy auth-profiles.json from an older aquaman-plugin blocks providers on this OpenClaw');
|
|
1886
|
+
console.log(' Run once: openclaw doctor --fix (archives it; aquaman-plugin 0.15.0+ never recreates it)');
|
|
1887
|
+
}
|
|
1672
1888
|
}
|
|
1673
1889
|
else if (!fs.existsSync(profilesPath)) {
|
|
1674
1890
|
const profiles = {};
|
|
@@ -1780,6 +1996,9 @@ openclaw
|
|
|
1780
1996
|
// 2. Backend accessible
|
|
1781
1997
|
let config;
|
|
1782
1998
|
let store = null;
|
|
1999
|
+
// Services the vault holds anything for. Used by the channel-routing
|
|
2000
|
+
// check below, which runs outside the store's try block.
|
|
2001
|
+
const vaultServices = new Set();
|
|
1783
2002
|
try {
|
|
1784
2003
|
config = loadConfig();
|
|
1785
2004
|
if (config.credentials.backend === 'systemd-creds') {
|
|
@@ -1812,6 +2031,8 @@ openclaw
|
|
|
1812
2031
|
});
|
|
1813
2032
|
// 3. Count credentials
|
|
1814
2033
|
const creds = await store.list();
|
|
2034
|
+
for (const c of creds)
|
|
2035
|
+
vaultServices.add(c.service);
|
|
1815
2036
|
if (creds.length > 0) {
|
|
1816
2037
|
const names = creds.map(c => `${c.service}/${c.key}`).join(', ');
|
|
1817
2038
|
console.log(` \u2713 ${aqua('Backend:')} ${config.credentials.backend} (accessible)`);
|
|
@@ -1916,6 +2137,12 @@ openclaw
|
|
|
1916
2137
|
console.log(` ${svc}: ${summary}`);
|
|
1917
2138
|
}
|
|
1918
2139
|
}
|
|
2140
|
+
// Deny rules that can never fire (pre-0.15.0 preset shapes) — a
|
|
2141
|
+
// security issue, since the operator believes the endpoint is blocked.
|
|
2142
|
+
for (const warning of lintPolicyConfig(policyConfigDoc)) {
|
|
2143
|
+
console.log(` \u2717 ${aqua('Policy')} ${warning}`);
|
|
2144
|
+
issues++;
|
|
2145
|
+
}
|
|
1919
2146
|
// Warn about policies for non-proxied services
|
|
1920
2147
|
if (config.credentials.proxiedServices) {
|
|
1921
2148
|
for (const svc of Object.keys(policyConfigDoc)) {
|
|
@@ -2020,9 +2247,21 @@ openclaw
|
|
|
2020
2247
|
if (fs.existsSync(openclawJsonPath)) {
|
|
2021
2248
|
try {
|
|
2022
2249
|
const openclawConfig = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf-8'));
|
|
2023
|
-
const allowList = openclawConfig.plugins?.allow
|
|
2024
|
-
if (allowList
|
|
2250
|
+
const allowList = Array.isArray(openclawConfig.plugins?.allow) ? openclawConfig.plugins.allow : undefined;
|
|
2251
|
+
if (!allowList) {
|
|
2252
|
+
console.log(` \u2713 ${aqua('Plugin')} allowed (no plugins.allow trust list; \`openclaw security audit\` notes extensions_no_allowlist)`);
|
|
2253
|
+
}
|
|
2254
|
+
else if (allowList.includes('aquaman-plugin')) {
|
|
2025
2255
|
console.log(` \u2713 ${aqua('Plugin')} in plugins.allow trust list`);
|
|
2256
|
+
// A list that excludes OpenClaw's own provider plugins blocks
|
|
2257
|
+
// them (aquaman setup <= 0.14.x wrote exactly such a list).
|
|
2258
|
+
const svcs = openclawConfig.plugins?.entries?.['aquaman-plugin']?.config?.services ?? ['anthropic', 'openai'];
|
|
2259
|
+
const blocked = ['anthropic', 'openai'].filter(p => svcs.includes(p) && !allowList.includes(p));
|
|
2260
|
+
if (blocked.length > 0 && supportsSecretRefIntegrations(openclawVersion)) {
|
|
2261
|
+
console.log(` \u2717 ${aqua('Plugin')} plugins.allow blocks OpenClaw\u2019s own provider plugin(s): ${blocked.join(', ')} (their models report "Unknown model")`);
|
|
2262
|
+
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)`);
|
|
2263
|
+
issues++;
|
|
2264
|
+
}
|
|
2026
2265
|
}
|
|
2027
2266
|
else {
|
|
2028
2267
|
console.log(` \u2717 ${aqua('Plugin')} not in plugins.allow trust list`);
|
|
@@ -2048,6 +2287,17 @@ openclaw
|
|
|
2048
2287
|
if (status.providerConfigured && status.missingProviders.length === 0) {
|
|
2049
2288
|
secretRefFullyWired = true;
|
|
2050
2289
|
console.log(` ✓ ${aqua('SecretRef')} wiring active (${status.wiredProviders.join(', ') || 'no providers'})`);
|
|
2290
|
+
// The apiKey ref alone doesn't put the proxy in the path: without
|
|
2291
|
+
// a loopback baseUrl OpenClaw's transport calls the upstream
|
|
2292
|
+
// directly and the placeholder fails there (v0.15.0).
|
|
2293
|
+
if (status.missingBaseUrl.length > 0) {
|
|
2294
|
+
console.log(` ✗ ${aqua('Model routing')} ${status.missingBaseUrl.join(', ')} still call the provider directly — the proxy is bypassed`);
|
|
2295
|
+
console.log(' → Run: aquaman openclaw setup (points models.providers.<svc>.baseUrl at the loopback listener)');
|
|
2296
|
+
issues++;
|
|
2297
|
+
}
|
|
2298
|
+
else if (status.baseUrlProviders.length > 0) {
|
|
2299
|
+
console.log(` ✓ ${aqua('Model routing')} through the proxy for ${status.baseUrlProviders.join(', ')}`);
|
|
2300
|
+
}
|
|
2051
2301
|
}
|
|
2052
2302
|
else if (status.providerConfigured) {
|
|
2053
2303
|
// Half-migrated: the provider block exists but some providers still
|
|
@@ -2068,6 +2318,42 @@ openclaw
|
|
|
2068
2318
|
// openclaw.json invalid — already reported above
|
|
2069
2319
|
}
|
|
2070
2320
|
}
|
|
2321
|
+
// 8.6. Channel egress routing (v0.15.0+). Channels build their own
|
|
2322
|
+
// undici dispatcher, so the plugin's fetch interceptor never sees
|
|
2323
|
+
// them. Only channels whose endpoint OpenClaw lets us override can
|
|
2324
|
+
// be routed; the rest are at-rest storage only and say so.
|
|
2325
|
+
if (fs.existsSync(openclawJsonPath)) {
|
|
2326
|
+
try {
|
|
2327
|
+
const openclawConfig = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf-8'));
|
|
2328
|
+
const routing = channelRoutingStatus(openclawConfig, {
|
|
2329
|
+
hasVaultCredential: (svc) => vaultServices.has(svc)
|
|
2330
|
+
});
|
|
2331
|
+
if (routing.routed.length > 0) {
|
|
2332
|
+
console.log(` \u2713 ${aqua('Channel routing')} through the proxy for ${routing.routed.join(', ')}`);
|
|
2333
|
+
// Routed but not proxied means our own proxy 404s the channel.
|
|
2334
|
+
const notProxied = routing.routed.filter(c => !(config?.credentials?.proxiedServices ?? []).includes(c));
|
|
2335
|
+
if (notProxied.length > 0) {
|
|
2336
|
+
console.log(` \u2717 ${aqua('Channel routing')} ${notProxied.join(', ')} routed here but missing from proxiedServices, so the proxy returns 404`);
|
|
2337
|
+
console.log(` \u2192 Add ${notProxied.join(', ')} to credentials.proxiedServices in ~/.aquaman/config.yaml (or re-run: aquaman openclaw setup)`);
|
|
2338
|
+
issues++;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
for (const skip of routing.notRouted) {
|
|
2342
|
+
console.log(` \u2717 ${aqua('Channel routing')} ${skip.channel} sends its token straight to the vendor`);
|
|
2343
|
+
console.log(` \u2192 ${describeChannelSkip(skip.reason, skip.channel)}`);
|
|
2344
|
+
issues++;
|
|
2345
|
+
}
|
|
2346
|
+
if (routing.unroutable.length > 0) {
|
|
2347
|
+
// Not a failure: nothing the user can do, and the credentials can
|
|
2348
|
+
// still live in the vault. Stated so nobody assumes coverage.
|
|
2349
|
+
console.log(` \u2192 ${aqua('Channel routing')} not available for ${routing.unroutable.join(', ')} on this OpenClaw (no endpoint override)`);
|
|
2350
|
+
console.log(' (their tokens can live in the vault, but the proxy is not in their egress path)');
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
// openclaw.json invalid \u2014 already reported above
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2071
2357
|
// 9. Auth profiles. With SecretRef wiring active this surface is
|
|
2072
2358
|
// legacy/inert. Otherwise: OpenClaw >= 2026.6.5 reads provider auth
|
|
2073
2359
|
// profiles from each agent's openclaw-agent.sqlite and no longer reads
|
|
@@ -2077,7 +2363,18 @@ openclaw
|
|
|
2077
2363
|
const agentDir = path.join(openclawStateDir, 'agents', 'main', 'agent');
|
|
2078
2364
|
const profilesPath = path.join(agentDir, 'auth-profiles.json');
|
|
2079
2365
|
const sqlitePath = path.join(agentDir, 'openclaw-agent.sqlite');
|
|
2080
|
-
if (
|
|
2366
|
+
if (legacyAuthProfilesBlockProviders(openclawVersion) && fs.existsSync(profilesPath)) {
|
|
2367
|
+
// OpenClaw 2.0 line: the legacy JSON is not inert any more — it blocks
|
|
2368
|
+
// providers with AUTH_PROFILE_MIGRATION_REQUIRED (#114033). Older
|
|
2369
|
+
// aquaman-plugin versions rewrote it on every load; 0.15.0+ doesn't.
|
|
2370
|
+
console.log(` \u2717 ${aqua('Auth profiles')} legacy auth-profiles.json blocks anthropic/openai on ${openclawVersion} (AUTH_PROFILE_MIGRATION_REQUIRED)`);
|
|
2371
|
+
if (!secretRefFullyWired) {
|
|
2372
|
+
console.log(' \u2192 First: aquaman openclaw setup (wires the SecretRef provider that replaces it)');
|
|
2373
|
+
}
|
|
2374
|
+
console.log(' \u2192 Then run once: openclaw doctor --fix (archives the file; aquaman-plugin 0.15.0+ never recreates it)');
|
|
2375
|
+
issues++;
|
|
2376
|
+
}
|
|
2377
|
+
else if (secretRefFullyWired) {
|
|
2081
2378
|
console.log(` ✓ ${aqua('Auth profiles')} not needed (SecretRef wiring replaces the placeholder flow)`);
|
|
2082
2379
|
if (fs.existsSync(profilesPath)) {
|
|
2083
2380
|
console.log(' → Optional: the legacy auth-profiles.json placeholder is inert and may be deleted');
|
|
@@ -2095,8 +2392,8 @@ openclaw
|
|
|
2095
2392
|
console.log(` \u2713 ${aqua('Auth profiles')} (SQLite store present; OpenClaw \u2265 2026.6.5)`);
|
|
2096
2393
|
}
|
|
2097
2394
|
else {
|
|
2098
|
-
console.log(` \u2717 ${aqua('Auth profiles')} missing
|
|
2099
|
-
console.log(' \u2192 Run: aquaman setup
|
|
2395
|
+
console.log(` \u2717 ${aqua('Auth profiles')} missing: no credential wiring for OpenClaw ${openclawVersion}`);
|
|
2396
|
+
console.log(' \u2192 Run: aquaman openclaw setup (wires the SecretRef provider; the plugin no longer writes auth-profiles.json here)');
|
|
2100
2397
|
issues++;
|
|
2101
2398
|
}
|
|
2102
2399
|
}
|
|
@@ -2584,6 +2881,48 @@ policy
|
|
|
2584
2881
|
console.log(` \u2717 DENIED by rule: ${rule.method} ${rule.path} \u2192 ${rule.action}`);
|
|
2585
2882
|
}
|
|
2586
2883
|
});
|
|
2884
|
+
// Credential broker scope (v0.15.0+)
|
|
2885
|
+
const broker = program
|
|
2886
|
+
.command('broker')
|
|
2887
|
+
.description('Credential broker scope: which refs `aquaman daemon` may hand out');
|
|
2888
|
+
broker
|
|
2889
|
+
.command('list')
|
|
2890
|
+
.description('List the refs the daemon broker will materialize, and where each is declared')
|
|
2891
|
+
.action(() => {
|
|
2892
|
+
const config = loadConfig();
|
|
2893
|
+
console.log('Credential broker: served by `aquaman daemon` only. OpenClaw-hosted proxies never hand out credential values.\n');
|
|
2894
|
+
if (config.broker?.enabled === false) {
|
|
2895
|
+
console.log(' Disabled (broker.enabled: false in config.yaml) — every broker request is refused.');
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const scope = daemonBrokerScope(config);
|
|
2899
|
+
const declared = scope.declared();
|
|
2900
|
+
if (declared.length === 0) {
|
|
2901
|
+
console.log(' No declared refs — every broker request is refused.');
|
|
2902
|
+
}
|
|
2903
|
+
for (const d of declared)
|
|
2904
|
+
console.log(` ${d.ref} (${d.source})`);
|
|
2905
|
+
const err = scope.declarationError();
|
|
2906
|
+
if (err)
|
|
2907
|
+
console.log(`\n ⚠ ${err} — refs declared there are refused until it parses`);
|
|
2908
|
+
console.log(`\n Never over the loopback listener: ${HERMES_SUPPORTED_SERVICES.join(', ')} (process-isolated on the proxy path)`);
|
|
2909
|
+
console.log(` Coding-agent refs: ${defaultProjectsPath()} (aquaman coder project add ...)`);
|
|
2910
|
+
console.log(` Other refs: broker.allowedRefs in ${getConfigPath()} (aquaman broker allow ...)`);
|
|
2911
|
+
});
|
|
2912
|
+
broker
|
|
2913
|
+
.command('allow')
|
|
2914
|
+
.description('Allow aquaman://service/key refs to be materialized (e.g. Hermes secret-source bindings)')
|
|
2915
|
+
.argument('<refs...>', 'one or more aquaman://service/key refs')
|
|
2916
|
+
.action((refs) => {
|
|
2917
|
+
updateBrokerAllowedRefs(refs, 'allow');
|
|
2918
|
+
});
|
|
2919
|
+
broker
|
|
2920
|
+
.command('revoke')
|
|
2921
|
+
.description('Stop materializing refs added with `aquaman broker allow`')
|
|
2922
|
+
.argument('<refs...>', 'one or more aquaman://service/key refs')
|
|
2923
|
+
.action((refs) => {
|
|
2924
|
+
updateBrokerAllowedRefs(refs, 'revoke');
|
|
2925
|
+
});
|
|
2587
2926
|
// Migration commands
|
|
2588
2927
|
openclaw
|
|
2589
2928
|
.command('migrate')
|