aquaman-proxy 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.js +163 -127
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/audit/logger.js +1 -1
  5. package/dist/core/audit/logger.js.map +1 -1
  6. package/dist/core/credentials/backends/onepassword.js +1 -1
  7. package/dist/core/credentials/backends/onepassword.js.map +1 -1
  8. package/dist/core/credentials/backends/systemd-creds.js +1 -1
  9. package/dist/core/credentials/backends/systemd-creds.js.map +1 -1
  10. package/dist/core/credentials/caching-store.d.ts +43 -0
  11. package/dist/core/credentials/caching-store.d.ts.map +1 -0
  12. package/dist/core/credentials/caching-store.js +77 -0
  13. package/dist/core/credentials/caching-store.js.map +1 -0
  14. package/dist/core/credentials/index.d.ts +1 -0
  15. package/dist/core/credentials/index.d.ts.map +1 -1
  16. package/dist/core/credentials/index.js +1 -0
  17. package/dist/core/credentials/index.js.map +1 -1
  18. package/dist/core/index.d.ts +2 -2
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/index.js +2 -2
  21. package/dist/core/index.js.map +1 -1
  22. package/dist/core/types.d.ts +9 -0
  23. package/dist/core/types.d.ts.map +1 -1
  24. package/dist/core/utils/config.d.ts +17 -0
  25. package/dist/core/utils/config.d.ts.map +1 -1
  26. package/dist/core/utils/config.js +33 -1
  27. package/dist/core/utils/config.js.map +1 -1
  28. package/dist/core/utils/index.d.ts +1 -1
  29. package/dist/core/utils/index.d.ts.map +1 -1
  30. package/dist/core/utils/index.js +1 -1
  31. package/dist/core/utils/index.js.map +1 -1
  32. package/dist/hermes/config-writer.d.ts +10 -0
  33. package/dist/hermes/config-writer.d.ts.map +1 -1
  34. package/dist/hermes/config-writer.js +31 -0
  35. package/dist/hermes/config-writer.js.map +1 -1
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/openclaw/integration.d.ts.map +1 -1
  41. package/dist/openclaw/integration.js +4 -0
  42. package/dist/openclaw/integration.js.map +1 -1
  43. package/dist/openclaw/secretref.d.ts +74 -0
  44. package/dist/openclaw/secretref.d.ts.map +1 -0
  45. package/dist/openclaw/secretref.js +139 -0
  46. package/dist/openclaw/secretref.js.map +1 -0
  47. package/package.json +3 -3
package/README.md CHANGED
@@ -18,7 +18,7 @@ Agent / OpenClaw / Coding Agent Aquaman Proxy
18
18
  │ No credentials. │ proxy.sock │ │
19
19
  │ No open ports. │ (chmod 0o600) │ │
20
20
  │ Nothing to steal. │ │ │
21
- └──────────────────────┘ └───┬──────────┬───────┘
21
+ └──────────────────────┘ └──┬──────────┬────────┘
22
22
  │ │
23
23
  │ ▼
24
24
  │ ~/.aquaman/audit/
package/dist/cli/index.js CHANGED
@@ -12,12 +12,14 @@ 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 } from '../core/index.js';
15
+ import { loadConfig, getConfigDir, ensureConfigDir, getDefaultConfig, expandPath, createAuditLogger, createCredentialStore, saveConfig, generateLoopbackToken, DEFAULT_LOOPBACK_PORT, wrapWithCache, resolveCacheTtl } from '../core/index.js';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { createCredentialProxy } from '../daemon.js';
18
18
  import { createServiceRegistry, ServiceRegistry } from '../service-registry.js';
19
19
  import { createOpenClawIntegration, authProfilesAreSqliteOnly } from '../openclaw/integration.js';
20
+ import { supportsSecretRefIntegrations, wireSecretRefProviders, secretRefWiringStatus } from '../openclaw/secretref.js';
20
21
  import { createHermesIntegration, detectHermes } from '../hermes/integration.js';
22
+ import { managedScopeShadowedKeys, HERMES_MANAGED_ENV_PATH } from '../hermes/config-writer.js';
21
23
  import { loadPolicyFromConfig, validatePolicyConfig, getDefaultPolicyPresets, matchPolicy } from '../request-policy.js';
22
24
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
23
25
  // Read version from package.json (single source of truth)
@@ -38,6 +40,49 @@ function validateCredName(label, value) {
38
40
  process.exit(1);
39
41
  }
40
42
  }
43
+ /**
44
+ * Soft hint for 1Password users running in biometric/desktop-app mode: every
45
+ * `op` spawn prompts, which makes unattended agents unusable. The daemon cache
46
+ * (v0.13.1+) bounds it to one prompt per TTL per credential; a service account
47
+ * removes prompts entirely. Printed by `aquaman doctor` and deep doctors.
48
+ */
49
+ function printOnePasswordModeHint(config, indent = ' ') {
50
+ if (config.credentials.backend !== '1password')
51
+ return;
52
+ if (process.env['OP_SERVICE_ACCOUNT_TOKEN'] || process.env['OP_CONNECT_HOST'])
53
+ return;
54
+ const vault = config.credentials.onePasswordVault || 'aquaman';
55
+ const ttl = resolveCacheTtl(config);
56
+ const cacheNote = ttl > 0
57
+ ? `the daemon cache bounds this to one per ${Math.round(ttl / 60)} min per credential`
58
+ : 'and the credential cache is disabled (cacheTtlSeconds: 0)';
59
+ console.log(`${indent}• 1Password is in biometric/desktop-app mode — each vault read prompts (${cacheNote}).`);
60
+ console.log(`${indent} For zero prompts, create a service account scoped to the "${vault}" vault (create the`);
61
+ console.log(`${indent} vault + credentials as a human first, or grant write_items) and start the daemon with`);
62
+ console.log(`${indent} OP_SERVICE_ACCOUNT_TOKEN. If OP_CONNECT_HOST/OP_CONNECT_TOKEN are set they take precedence.`);
63
+ }
64
+ /**
65
+ * Map config.credentials onto CredentialStoreOptions — the single source for
66
+ * every createCredentialStore() call in this CLI. Env fallbacks mirror
67
+ * applyEnvOverrides() (belt and braces for callers that build partial configs).
68
+ */
69
+ function storeOptionsFromConfig(config) {
70
+ return {
71
+ backend: config.credentials.backend,
72
+ encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
73
+ vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
74
+ vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
75
+ vaultNamespace: config.credentials.vaultNamespace,
76
+ vaultMountPath: config.credentials.vaultMountPath,
77
+ onePasswordVault: config.credentials.onePasswordVault,
78
+ onePasswordAccount: config.credentials.onePasswordAccount,
79
+ keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
80
+ keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
81
+ bitwardenFolder: config.credentials.bitwardenFolder,
82
+ bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
83
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId
84
+ };
85
+ }
41
86
  /**
42
87
  * Build loopback listener options from config for the daemon. Returns undefined
43
88
  * unless the loopback listener is explicitly enabled. If it's enabled but no
@@ -310,21 +355,10 @@ program
310
355
  // Vault \u2014 backend + creds
311
356
  try {
312
357
  const config = loadConfig();
313
- const store = await createCredentialStore({
314
- backend: config.credentials.backend,
315
- encryptionPassword: config.credentials.encryptionPassword,
316
- vaultAddress: config.credentials.vaultAddress,
317
- vaultToken: config.credentials.vaultToken,
318
- onePasswordVault: config.credentials.onePasswordVault,
319
- onePasswordAccount: config.credentials.onePasswordAccount,
320
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
321
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
322
- bitwardenFolder: config.credentials.bitwardenFolder,
323
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
324
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
325
- });
358
+ const store = await createCredentialStore(storeOptionsFromConfig(config));
326
359
  const creds = await store.list();
327
360
  console.log(` \u2713 ${config.credentials.backend} backend (${creds.length} credential${creds.length !== 1 ? 's' : ''})`);
361
+ printOnePasswordModeHint(config);
328
362
  }
329
363
  catch (err) {
330
364
  console.log(` \u2717 Backend not accessible: ${err.message}`);
@@ -528,19 +562,7 @@ async function runVaultSetup(opts) {
528
562
  fs.mkdirSync(auditDir, { recursive: true, mode: 0o700 });
529
563
  let store;
530
564
  try {
531
- store = await createCredentialStore({
532
- backend: config.credentials.backend,
533
- encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
534
- vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
535
- vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
536
- onePasswordVault: config.credentials.onePasswordVault,
537
- onePasswordAccount: config.credentials.onePasswordAccount,
538
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
539
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
540
- bitwardenFolder: config.credentials.bitwardenFolder,
541
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
542
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
543
- });
565
+ store = await createCredentialStore(storeOptionsFromConfig(config));
544
566
  }
545
567
  catch (err) {
546
568
  console.error(` Failed to initialize ${backend}: ${err instanceof Error ? err.message : err}`);
@@ -666,21 +688,7 @@ openclaw
666
688
  // Initialize credential store
667
689
  let credentialStore;
668
690
  try {
669
- credentialStore = await createCredentialStore({
670
- backend: config.credentials.backend,
671
- encryptionPassword: config.credentials.encryptionPassword,
672
- vaultAddress: config.credentials.vaultAddress,
673
- vaultToken: config.credentials.vaultToken,
674
- vaultNamespace: config.credentials.vaultNamespace,
675
- vaultMountPath: config.credentials.vaultMountPath,
676
- onePasswordVault: config.credentials.onePasswordVault,
677
- onePasswordAccount: config.credentials.onePasswordAccount,
678
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
679
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
680
- bitwardenFolder: config.credentials.bitwardenFolder,
681
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
682
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
683
- });
691
+ credentialStore = wrapWithCache(await createCredentialStore(storeOptionsFromConfig(config)), resolveCacheTtl(config));
684
692
  }
685
693
  catch (err) {
686
694
  console.error(`Credential backend "${config.credentials.backend}" failed to initialize: ${err instanceof Error ? err.message : err}`);
@@ -822,21 +830,7 @@ program
822
830
  // Initialize credential store
823
831
  let credentialStore;
824
832
  try {
825
- credentialStore = await createCredentialStore({
826
- backend: config.credentials.backend,
827
- encryptionPassword: config.credentials.encryptionPassword,
828
- vaultAddress: config.credentials.vaultAddress,
829
- vaultToken: config.credentials.vaultToken,
830
- vaultNamespace: config.credentials.vaultNamespace,
831
- vaultMountPath: config.credentials.vaultMountPath,
832
- onePasswordVault: config.credentials.onePasswordVault,
833
- onePasswordAccount: config.credentials.onePasswordAccount,
834
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
835
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
836
- bitwardenFolder: config.credentials.bitwardenFolder,
837
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
838
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
839
- });
833
+ credentialStore = wrapWithCache(await createCredentialStore(storeOptionsFromConfig(config)), resolveCacheTtl(config));
840
834
  }
841
835
  catch (err) {
842
836
  console.error(`Credential backend "${config.credentials.backend}" failed to initialize: ${err instanceof Error ? err.message : err}`);
@@ -896,21 +890,7 @@ openclaw
896
890
  // Initialize credential store
897
891
  let credentialStore;
898
892
  try {
899
- credentialStore = await createCredentialStore({
900
- backend: config.credentials.backend,
901
- encryptionPassword: config.credentials.encryptionPassword,
902
- vaultAddress: config.credentials.vaultAddress,
903
- vaultToken: config.credentials.vaultToken,
904
- vaultNamespace: config.credentials.vaultNamespace,
905
- vaultMountPath: config.credentials.vaultMountPath,
906
- onePasswordVault: config.credentials.onePasswordVault,
907
- onePasswordAccount: config.credentials.onePasswordAccount,
908
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
909
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
910
- bitwardenFolder: config.credentials.bitwardenFolder,
911
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
912
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
913
- });
893
+ credentialStore = wrapWithCache(await createCredentialStore(storeOptionsFromConfig(config)), resolveCacheTtl(config));
914
894
  }
915
895
  catch (err) {
916
896
  console.error(`Credential backend "${config.credentials.backend}" failed to initialize: ${err instanceof Error ? err.message : err}`);
@@ -1104,6 +1084,10 @@ hermes
1104
1084
  }
1105
1085
  catch { /* unreadable */ }
1106
1086
  console.log(` ~/.hermes/.env: ${envWired ? aqua('wired') : 'not wired'} (${envPath})`);
1087
+ const shadowed = managedScopeShadowedKeys(integration.configureHermes());
1088
+ if (shadowed.length > 0) {
1089
+ console.log(` Managed scope: ⚠ ${HERMES_MANAGED_ENV_PATH} pins ${shadowed.join(', ')} — proxy bypassed for these keys`);
1090
+ }
1107
1091
  const info = await detectHermes(config.hermes?.binaryPath || 'hermes');
1108
1092
  console.log(` Hermes CLI: ${info.installed ? aqua(`v${info.version}`) : 'not found'}`);
1109
1093
  // Live liveness probe of the loopback listener (no token needed for _health).
@@ -1162,6 +1146,16 @@ hermes
1162
1146
  catch {
1163
1147
  fail(`Could not read ${envPath}`);
1164
1148
  }
1149
+ // 3b. Hermes >=0.17 managed scope: /etc/hermes/.env overrides ~/.hermes/.env
1150
+ {
1151
+ const shadowed = managedScopeShadowedKeys(integration.configureHermes());
1152
+ if (shadowed.length > 0) {
1153
+ fail(`Hermes managed scope (${HERMES_MANAGED_ENV_PATH}) pins ${shadowed.join(', ')} — it overrides ~/.hermes/.env, so the proxy is BYPASSED for these keys. Remove them from the managed file or point them at the proxy.`);
1154
+ }
1155
+ else {
1156
+ pass('No managed-scope override of aquaman env vars');
1157
+ }
1158
+ }
1165
1159
  // 4. Per-service vault credentials resolve
1166
1160
  const wired = integration.wiredServices();
1167
1161
  if (wired.length === 0) {
@@ -1169,21 +1163,7 @@ hermes
1169
1163
  }
1170
1164
  else {
1171
1165
  try {
1172
- const store = await createCredentialStore({
1173
- backend: config.credentials.backend,
1174
- encryptionPassword: config.credentials.encryptionPassword,
1175
- vaultAddress: config.credentials.vaultAddress,
1176
- vaultToken: config.credentials.vaultToken,
1177
- vaultNamespace: config.credentials.vaultNamespace,
1178
- vaultMountPath: config.credentials.vaultMountPath,
1179
- onePasswordVault: config.credentials.onePasswordVault,
1180
- onePasswordAccount: config.credentials.onePasswordAccount,
1181
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
1182
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
1183
- bitwardenFolder: config.credentials.bitwardenFolder,
1184
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
1185
- bitwardenCollectionId: config.credentials.bitwardenCollectionId,
1186
- });
1166
+ const store = await createCredentialStore(storeOptionsFromConfig(config));
1187
1167
  for (const svc of wired) {
1188
1168
  const val = await store.get(svc, 'api_key');
1189
1169
  if (val)
@@ -1191,6 +1171,7 @@ hermes
1191
1171
  else
1192
1172
  fail(`No credential for ${svc}/api_key — run: aquaman credentials add ${svc} api_key`);
1193
1173
  }
1174
+ printOnePasswordModeHint(config, ' ');
1194
1175
  }
1195
1176
  catch (err) {
1196
1177
  fail(`Vault backend "${config.credentials.backend}" failed: ${err instanceof Error ? err.message : err}`);
@@ -1379,7 +1360,7 @@ openclaw
1379
1360
  process.exit(1);
1380
1361
  }
1381
1362
  }
1382
- catch (err) {
1363
+ catch {
1383
1364
  console.error(` Cannot reach Vault at ${vaultAddr}.`);
1384
1365
  process.exit(1);
1385
1366
  }
@@ -1459,19 +1440,7 @@ openclaw
1459
1440
  // 3. Prompt for API keys (or read from env in non-interactive mode)
1460
1441
  let store;
1461
1442
  try {
1462
- store = await createCredentialStore({
1463
- backend: config.credentials.backend,
1464
- encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
1465
- vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
1466
- vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
1467
- onePasswordVault: config.credentials.onePasswordVault,
1468
- onePasswordAccount: config.credentials.onePasswordAccount,
1469
- keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
1470
- keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
1471
- bitwardenFolder: config.credentials.bitwardenFolder,
1472
- bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
1473
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
1474
- });
1443
+ store = await createCredentialStore(storeOptionsFromConfig(config));
1475
1444
  }
1476
1445
  catch (err) {
1477
1446
  console.error(` Failed to initialize ${backend} credential store: ${err instanceof Error ? err.message : err}`);
@@ -1657,22 +1626,54 @@ openclaw
1657
1626
  if (!openclawConfig.plugins.allow.includes('aquaman-plugin')) {
1658
1627
  openclawConfig.plugins.allow.push('aquaman-plugin');
1659
1628
  }
1629
+ const configuredServices = storedServices.length > 0 ? storedServices : ['anthropic', 'openai'];
1660
1630
  openclawConfig.plugins.entries['aquaman-plugin'] = {
1661
1631
  enabled: true,
1662
1632
  config: {
1663
1633
  backend,
1664
- services: storedServices.length > 0 ? storedServices : ['anthropic', 'openai'],
1634
+ services: configuredServices,
1665
1635
  }
1666
1636
  };
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
+ // b2. SecretRef provider wiring (v0.14.0+, OpenClaw >= 2026.6.5).
1650
+ // Config-level refs in openclaw.json are runtime-read on every
1651
+ // version and survive OpenClaw's plaintext scrubs \u2014 no SQLite
1652
+ // import step needed, unlike the legacy placeholder.
1653
+ const secretRefSupported = supportsSecretRefIntegrations(ocVersion);
1654
+ if (secretRefSupported) {
1655
+ const wiring = wireSecretRefProviders(openclawConfig, configuredServices);
1656
+ if (wiring.wiredProviders.length > 0) {
1657
+ console.log(` \u2713 SecretRef wiring for ${wiring.wiredProviders.join(', ')} (canonical credential surface)`);
1658
+ }
1659
+ const userKept = wiring.skippedProviders.filter(s => s === 'anthropic' || s === 'openai');
1660
+ if (userKept.length > 0) {
1661
+ console.log(` \u2192 Left existing user-set apiKey untouched for: ${userKept.join(', ')}`);
1662
+ }
1663
+ }
1667
1664
  fs.writeFileSync(openclawJsonPath, JSON.stringify(openclawConfig, null, 2), 'utf-8');
1668
1665
  console.log(' \u2713 Plugin config written to ' + openclawJsonPath);
1669
- // c. Generate auth-profiles.json (only if missing)
1666
+ // c. Generate auth-profiles.json (only if missing). With SecretRef
1667
+ // wiring active this is a redundant legacy surface \u2014 skip it so
1668
+ // `openclaw secrets audit` has no plaintext placeholder to flag.
1670
1669
  const profilesPath = path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json');
1671
- if (!fs.existsSync(profilesPath)) {
1670
+ if (secretRefSupported) {
1671
+ console.log(' \u2192 Legacy auth-profiles.json placeholder skipped (SecretRef wiring replaces it)');
1672
+ }
1673
+ else if (!fs.existsSync(profilesPath)) {
1672
1674
  const profiles = {};
1673
1675
  const order = {};
1674
- const profileServices = storedServices.length > 0 ? storedServices : ['anthropic', 'openai'];
1675
- for (const svc of profileServices) {
1676
+ for (const svc of configuredServices) {
1676
1677
  if (svc === 'anthropic' || svc === 'openai') {
1677
1678
  profiles[`${svc}:default`] = {
1678
1679
  type: 'api_key',
@@ -1688,20 +1689,13 @@ openclaw
1688
1689
  console.log(' \u2713 Auth profiles generated at ' + profilesPath);
1689
1690
  // OpenClaw >= 2026.6.5 reads provider auth profiles from each agent's
1690
1691
  // openclaw-agent.sqlite, not auth-profiles.json (openclaw/openclaw#89102).
1691
- // The placeholder above must be imported into SQLite once with
1692
- // `openclaw doctor --fix`. We deliberately do NOT auto-run that here:
1692
+ // On those versions the SecretRef branch above applies instead; this
1693
+ // hint only fires when the version couldn't be detected (SecretRef
1694
+ // gate returned false) but the runtime turns out to be >= 2026.6.5.
1695
+ // We deliberately do NOT auto-run `openclaw doctor --fix` here:
1693
1696
  // it is OpenClaw's broad config-healing command \u2014 verified to take
1694
1697
  // ~19s (gateway probes) and to rewrite openclaw.json (it reset our
1695
- // plugin entry to bundled defaults in testing). `aquaman openclaw
1696
- // doctor` detects the SQLite-only case and prints the import step so
1697
- // the operator runs it intentionally. Holistic fix (SecretRef
1698
- // provider manifest) is tracked for the next plugin touch.
1699
- let ocVersion = null;
1700
- try {
1701
- const { execSync } = await import('node:child_process');
1702
- ocVersion = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
1703
- }
1704
- catch { /* CLI not on PATH */ }
1698
+ // plugin entry to bundled defaults in testing).
1705
1699
  if (authProfilesAreSqliteOnly(ocVersion)) {
1706
1700
  console.log(' \u2192 OpenClaw \u2265 2026.6.5 reads auth profiles from SQLite; import the placeholder once:');
1707
1701
  console.log(' openclaw doctor --fix (backs up openclaw.json first; see aquaman openclaw doctor)');
@@ -1943,9 +1937,10 @@ openclaw
1943
1937
  console.log(` \u2139 ${aqua('Policy')} not configured \u2014 agents can call any endpoint on proxied services`);
1944
1938
  console.log(' \u2192 Run: aquaman setup (or add policy rules to ~/.aquaman/config.yaml)');
1945
1939
  }
1946
- // 6. OpenClaw detection
1940
+ // 6. OpenClaw detection. AQUAMAN_OPENCLAW_VERSION overrides version
1941
+ // detection (tests + operators pinning behavior explicitly).
1947
1942
  let openclawDetected = false;
1948
- let openclawVersion = null;
1943
+ let openclawVersion = process.env.AQUAMAN_OPENCLAW_VERSION || null;
1949
1944
  let cliFound = false;
1950
1945
  try {
1951
1946
  const { execSync } = await import('node:child_process');
@@ -1956,7 +1951,8 @@ openclaw
1956
1951
  if (cliFound) {
1957
1952
  try {
1958
1953
  const { execSync } = await import('node:child_process');
1959
- const ver = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
1954
+ const ver = openclawVersion ??
1955
+ execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
1960
1956
  openclawVersion = ver;
1961
1957
  console.log(` \u2713 ${aqua('OpenClaw')} installed (${ver})`);
1962
1958
  }
@@ -2040,15 +2036,55 @@ openclaw
2040
2036
  // openclaw.json invalid — already reported above
2041
2037
  }
2042
2038
  }
2043
- // 9. Auth profiles. OpenClaw >= 2026.6.5 reads provider auth profiles from
2044
- // each agent's openclaw-agent.sqlite and no longer reads
2045
- // auth-profiles.json at runtime (openclaw/openclaw#89102). On those
2046
- // versions the placeholder the plugin writes to the JSON file must be
2047
- // imported into SQLite once via `openclaw doctor --fix`.
2039
+ // 8.5. SecretRef provider wiring (v0.14.0+, OpenClaw >= 2026.6.5) — the
2040
+ // canonical credential surface that replaces the placeholder
2041
+ // auth-profiles dance.
2042
+ let secretRefFullyWired = false;
2043
+ if (supportsSecretRefIntegrations(openclawVersion) && fs.existsSync(openclawJsonPath)) {
2044
+ try {
2045
+ const openclawConfig = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf-8'));
2046
+ const pluginServices = openclawConfig.plugins?.entries?.['aquaman-plugin']?.config?.services ?? ['anthropic', 'openai'];
2047
+ const status = secretRefWiringStatus(openclawConfig, pluginServices);
2048
+ if (status.providerConfigured && status.missingProviders.length === 0) {
2049
+ secretRefFullyWired = true;
2050
+ console.log(` ✓ ${aqua('SecretRef')} wiring active (${status.wiredProviders.join(', ') || 'no providers'})`);
2051
+ }
2052
+ else if (status.providerConfigured) {
2053
+ // Half-migrated: the provider block exists but some providers still
2054
+ // lack refs — a real inconsistency, fail the check.
2055
+ console.log(` ✗ ${aqua('SecretRef')} wiring incomplete (providers not wired: ${status.missingProviders.join(', ')})`);
2056
+ console.log(' → Run: aquaman openclaw setup (completes the SecretRef wiring in openclaw.json)');
2057
+ issues++;
2058
+ }
2059
+ else {
2060
+ // Legacy placeholder flow still functional — upgrade suggestion,
2061
+ // not a failure. (OpenClaw's configure-flow scrubs treat the
2062
+ // plaintext placeholder as residue; SecretRef wiring is immune.)
2063
+ console.log(` → ${aqua('SecretRef')} wiring available on this OpenClaw — run: aquaman openclaw setup`);
2064
+ console.log(' (replaces the legacy plaintext placeholder, which OpenClaw now scrubs by default)');
2065
+ }
2066
+ }
2067
+ catch {
2068
+ // openclaw.json invalid — already reported above
2069
+ }
2070
+ }
2071
+ // 9. Auth profiles. With SecretRef wiring active this surface is
2072
+ // legacy/inert. Otherwise: OpenClaw >= 2026.6.5 reads provider auth
2073
+ // profiles from each agent's openclaw-agent.sqlite and no longer reads
2074
+ // auth-profiles.json at runtime (openclaw/openclaw#89102) — the
2075
+ // placeholder the plugin writes to the JSON file must be imported
2076
+ // into SQLite once via `openclaw doctor --fix`.
2048
2077
  const agentDir = path.join(openclawStateDir, 'agents', 'main', 'agent');
2049
2078
  const profilesPath = path.join(agentDir, 'auth-profiles.json');
2050
2079
  const sqlitePath = path.join(agentDir, 'openclaw-agent.sqlite');
2051
- if (authProfilesAreSqliteOnly(openclawVersion)) {
2080
+ if (secretRefFullyWired) {
2081
+ console.log(` ✓ ${aqua('Auth profiles')} not needed (SecretRef wiring replaces the placeholder flow)`);
2082
+ if (fs.existsSync(profilesPath)) {
2083
+ console.log(' → Optional: the legacy auth-profiles.json placeholder is inert and may be deleted');
2084
+ console.log(' (`openclaw secrets audit` flags its plaintext placeholder as PLAINTEXT_FOUND)');
2085
+ }
2086
+ }
2087
+ else if (authProfilesAreSqliteOnly(openclawVersion)) {
2052
2088
  if (fs.existsSync(profilesPath)) {
2053
2089
  console.log(` \u2717 ${aqua('Auth profiles')} JSON present but ${openclawVersion} reads from SQLite`);
2054
2090
  console.log(' \u2192 Import the placeholder once: openclaw doctor --fix (backs up openclaw.json first)');