aquaman-proxy 0.15.0 → 0.17.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 (38) hide show
  1. package/README.md +12 -1
  2. package/dist/cli/index.js +341 -18
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/credentials/backends/keeper.d.ts +69 -0
  5. package/dist/core/credentials/backends/keeper.d.ts.map +1 -0
  6. package/dist/core/credentials/backends/keeper.js +189 -0
  7. package/dist/core/credentials/backends/keeper.js.map +1 -0
  8. package/dist/core/credentials/store.d.ts +2 -0
  9. package/dist/core/credentials/store.d.ts.map +1 -1
  10. package/dist/core/credentials/store.js +7 -0
  11. package/dist/core/credentials/store.js.map +1 -1
  12. package/dist/core/redactor/index.d.ts.map +1 -1
  13. package/dist/core/redactor/index.js +10 -0
  14. package/dist/core/redactor/index.js.map +1 -1
  15. package/dist/core/types.d.ts +15 -3
  16. package/dist/core/types.d.ts.map +1 -1
  17. package/dist/core/utils/config.d.ts +2 -1
  18. package/dist/core/utils/config.d.ts.map +1 -1
  19. package/dist/core/utils/config.js +19 -4
  20. package/dist/core/utils/config.js.map +1 -1
  21. package/dist/openshell/credential-driver.d.ts +117 -0
  22. package/dist/openshell/credential-driver.d.ts.map +1 -0
  23. package/dist/openshell/credential-driver.js +181 -0
  24. package/dist/openshell/credential-driver.js.map +1 -0
  25. package/dist/openshell/grpc-server.d.ts +23 -0
  26. package/dist/openshell/grpc-server.d.ts.map +1 -0
  27. package/dist/openshell/grpc-server.js +122 -0
  28. package/dist/openshell/grpc-server.js.map +1 -0
  29. package/dist/openshell/integration.d.ts +37 -0
  30. package/dist/openshell/integration.d.ts.map +1 -0
  31. package/dist/openshell/integration.js +69 -0
  32. package/dist/openshell/integration.js.map +1 -0
  33. package/package.json +13 -4
  34. package/proto/openshell/README.md +9 -0
  35. package/proto/openshell/credential_driver.proto +146 -0
  36. package/proto/openshell/datamodel.proto +128 -0
  37. package/proto/openshell/extension.proto +34 -0
  38. package/proto/openshell/options.proto +36 -0
package/README.md CHANGED
@@ -78,7 +78,7 @@ The Telegram Bot API has no auth header, so there the token travels in the `/bot
78
78
 
79
79
  Trade-off: any local process can reach a loopback port, including other users, where the socket's `0600` shuts them out. The token is the gate there, so the listener stays off until `aquaman hermes setup` or `aquaman openclaw setup` turns it on.
80
80
 
81
- **Bring your own vault.** Aquaman has no house vault. It injects credentials from the secret store you already run: Keychain, 1Password, HashiCorp Vault, Bitwarden, KeePassXC, systemd-creds, or encrypted-file. No new store to adopt, no migration.
81
+ **Bring your own vault.** Aquaman has no house vault. It injects credentials from the secret store you already run: Keychain, 1Password, HashiCorp Vault, Bitwarden, Keeper, KeePassXC, systemd-creds, or encrypted-file. No new store to adopt, no migration.
82
82
 
83
83
  ## Broker endpoint (v0.12.0+)
84
84
 
@@ -97,6 +97,17 @@ Scoped since v0.15.0, because it hands out values rather than injecting them:
97
97
  - Over the loopback listener the LLM provider keys are never materialized, declared or not (`404 broker_ref_isolated`). They stay on the proxy path.
98
98
  - Declaring a ref is an explicit opt-in to handing that value to processes running as you. Refusals and resolves are both audited.
99
99
 
100
+ `aquaman get <aquaman://service/key>` (v0.16.0+) is the command-line form, for tools that take a command printing a secret: Docker Sandboxes (`sbx secret set <svc> --command`), Codex (`model_providers.<id>.auth.command`), Claude Code (`apiKeyHelper`), OpenClaw exec secret providers and Hermes command secret sources. It only talks to the running daemon, so the same scope applies and every read is audited. It refuses to print to an interactive terminal without `--show`, and prints no trailing newline when another program reads it. The value goes to the program that runs the command.
101
+
102
+ ## OpenShell credential driver (v0.17.0+)
103
+
104
+ `aquaman daemon` can serve NVIDIA OpenShell's external credential driver protocol (`credential_driver.proto`, extension protocol 1.0) on `<configDir>/openshell.sock` (0600). Enable it with `aquaman openshell setup`, which prints the gateway TOML.
105
+
106
+ - **Reference mode:** `openshell provider create --credential KEY=aquaman://svc/key`. The ref must be declared (`aquaman broker allow`) and exist in the vault; nothing is stored, the ref is the handle, and deleting the provider never deletes the vault item.
107
+ - **Copy mode:** a plain value is written to the vault under the `openshell` service.
108
+ - Every resolve is audited as a `read` (agent `openshell`), without the value. Handles are interpreted by namespace, never by gateway-supplied metadata, so a reference can't bypass the allow-list.
109
+ - `@grpc/grpc-js` and `@grpc/proto-loader` are optional peer dependencies, loaded only when the driver is on. The vendored protocol files live in `proto/openshell/`.
110
+
100
111
  ## Documentation
101
112
 
102
113
  - **[Root README](https://github.com/tech4242/aquaman#readme)**: value prop, three-path Quick Start, security model
package/dist/cli/index.js CHANGED
@@ -82,7 +82,9 @@ function storeOptionsFromConfig(config) {
82
82
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
83
83
  bitwardenFolder: config.credentials.bitwardenFolder,
84
84
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
85
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
85
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
86
+ keeperFolderUid: config.credentials.keeperFolderUid,
87
+ keeperConfigPath: config.credentials.keeperConfigPath
86
88
  };
87
89
  }
88
90
  /**
@@ -151,6 +153,58 @@ function updateBrokerAllowedRefs(refs, mode) {
151
153
  console.log('Note: broker.enabled is false in config.yaml, so the broker is off regardless.');
152
154
  }
153
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
+ }
154
208
  function describeBroker(scope) {
155
209
  if (!scope)
156
210
  return 'disabled (broker.enabled: false)';
@@ -312,7 +366,7 @@ program
312
366
  }
313
367
  lines.push('');
314
368
  lines.push(aqua('Vault management'));
315
- for (const name of ['credentials', 'audit', 'services', 'policy']) {
369
+ for (const name of ['credentials', 'get', 'broker', 'audit', 'services', 'policy']) {
316
370
  const c = byName.get(name);
317
371
  if (c)
318
372
  lines.push(renderCmd(c));
@@ -336,13 +390,12 @@ program
336
390
  }
337
391
  // --- Coder namespace (shim \u2014 list documented subcommands) ---
338
392
  if (byName.has('coder')) {
339
- lines.push(aqua('AI coding-agent integration (Claude Code today)'));
393
+ lines.push(aqua('AI coding-agent integration (Claude Code, Codex)'));
340
394
  const coderSubs = [
341
- ['coder setup <agent>', 'Install hooks for an agent (claude-code today)'],
395
+ ['coder setup <agent>', 'Install hooks for an agent (claude-code, codex)'],
342
396
  ['coder doctor', 'Deep diagnostic \u2014 projects, broker, per-project vault'],
343
397
  ['coder status', 'Configured projects + hook wiring + broker activity'],
344
398
  ['coder project list/add/remove', 'Manage ~/.aquaman/projects.yaml'],
345
- ['coder get <ref>', 'Resolve an aquaman://service/key reference'],
346
399
  ['coder exec <cmd>', 'Run command with project env injected + output redacted'],
347
400
  ];
348
401
  for (const [term, desc] of coderSubs) {
@@ -366,6 +419,14 @@ program
366
419
  }
367
420
  lines.push('');
368
421
  }
422
+ const os_ = byName.get('openshell');
423
+ if (os_) {
424
+ lines.push(aqua('NVIDIA OpenShell integration'));
425
+ for (const sub of helper.visibleCommands(os_).filter((s) => s.name() !== 'help')) {
426
+ lines.push(renderRow(`openshell ${sub.name()}`, sub.description() || ''));
427
+ }
428
+ lines.push('');
429
+ }
369
430
  // --- Other ---
370
431
  const helpCmd = byName.get('help');
371
432
  if (helpCmd) {
@@ -389,7 +450,7 @@ program
389
450
  program
390
451
  .command('setup')
391
452
  .description('Vault-only setup wizard \u2014 backend + credentials (use `aquaman openclaw setup` or `aquaman coder setup` for full bundles)')
392
- .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
453
+ .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
393
454
  .option('--no-policy', 'Skip request policy preset configuration')
394
455
  .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
395
456
  .action(async (options) => {
@@ -614,7 +675,7 @@ async function runVaultSetup(opts) {
614
675
  }
615
676
  }
616
677
  }
617
- const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
678
+ const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden', 'keeper'];
618
679
  if (!validBackends.includes(backend)) {
619
680
  console.error(` Invalid backend: ${backend}. Valid: ${validBackends.join(', ')}`);
620
681
  process.exit(1);
@@ -630,6 +691,9 @@ async function runVaultSetup(opts) {
630
691
  config = loadConfig();
631
692
  }
632
693
  config.credentials.backend = backend;
694
+ if (backend === 'keeper' && process.env['AQUAMAN_KEEPER_FOLDER_UID']) {
695
+ config.credentials.keeperFolderUid = process.env['AQUAMAN_KEEPER_FOLDER_UID'];
696
+ }
633
697
  fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
634
698
  const auditDir = path.join(configDir, 'audit');
635
699
  fs.mkdirSync(auditDir, { recursive: true, mode: 0o700 });
@@ -785,7 +849,8 @@ openclaw
785
849
  onRequest: (info) => {
786
850
  auditLogger.logCredentialAccess('system', 'system', {
787
851
  service: info.service,
788
- operation: 'use',
852
+ // A broker resolve hands the value out; everything else injects it.
853
+ operation: info.method === 'BROKER' ? 'read' : 'use',
789
854
  success: !info.error,
790
855
  error: info.error
791
856
  });
@@ -931,17 +996,35 @@ program
931
996
  onRequest: (info) => {
932
997
  auditLogger.logCredentialAccess('system', 'system', {
933
998
  service: info.service,
934
- operation: 'use',
999
+ // A broker resolve hands the value out; everything else injects it.
1000
+ operation: info.method === 'BROKER' ? 'read' : 'use',
935
1001
  success: !info.error,
936
1002
  error: info.error
937
1003
  });
938
1004
  }
939
1005
  });
940
1006
  await credentialProxy.start();
1007
+ // OpenShell external credential driver (v0.17.0+). Opt-in; a failure here
1008
+ // is reported but never takes the proxy down.
1009
+ let openshellDriver;
1010
+ let openshellDriverError;
1011
+ if (config.openshell?.driver?.enabled) {
1012
+ try {
1013
+ const { startOpenShellDriver } = await import('../openshell/integration.js');
1014
+ openshellDriver = await startOpenShellDriver({ config, store: credentialStore, scope: brokerScope, audit: auditLogger, version: VERSION });
1015
+ }
1016
+ catch (err) {
1017
+ openshellDriverError = err instanceof Error ? err.message : String(err);
1018
+ }
1019
+ }
941
1020
  // Write PID file
942
1021
  writePidFile();
943
1022
  console.log(`Credential proxy: ${socketPath}`);
944
1023
  console.log(`Credential broker: ${describeBroker(brokerScope)}`);
1024
+ if (openshellDriver)
1025
+ console.log(`OpenShell credential driver: ${openshellDriver.socketPath}`);
1026
+ if (openshellDriverError)
1027
+ console.error(` ⚠ OpenShell credential driver not started: ${openshellDriverError}`);
945
1028
  if (brokerScope?.declarationError()) {
946
1029
  console.error(` ⚠ ${brokerScope.declarationError()} — refs declared there are refused until it parses`);
947
1030
  }
@@ -953,6 +1036,7 @@ program
953
1036
  // Handle shutdown
954
1037
  const shutdown = async () => {
955
1038
  console.log('\nShutting down daemon...');
1039
+ await openshellDriver?.stop();
956
1040
  await credentialProxy.stop();
957
1041
  removePidFile();
958
1042
  try {
@@ -1004,7 +1088,8 @@ openclaw
1004
1088
  onRequest: (info) => {
1005
1089
  auditLogger.logCredentialAccess('system', 'system', {
1006
1090
  service: info.service,
1007
- operation: 'use',
1091
+ // A broker resolve hands the value out; everything else injects it.
1092
+ operation: info.method === 'BROKER' ? 'read' : 'use',
1008
1093
  success: !info.error,
1009
1094
  error: info.error
1010
1095
  });
@@ -1382,7 +1467,7 @@ program
1382
1467
  openclaw
1383
1468
  .command('setup')
1384
1469
  .description('Full setup for OpenClaw — vault + plugin + auth profiles + (optional) auto-migrate')
1385
- .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
1470
+ .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
1386
1471
  .option('--no-openclaw', 'Skip OpenClaw plugin installation step (run vault setup only)')
1387
1472
  .option('--no-policy', 'Skip request policy preset configuration')
1388
1473
  .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
@@ -1429,7 +1514,7 @@ openclaw
1429
1514
  }
1430
1515
  }
1431
1516
  // Validate backend
1432
- const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
1517
+ const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden', 'keeper'];
1433
1518
  if (!validBackends.includes(backend)) {
1434
1519
  console.error(` Invalid backend: ${backend}`);
1435
1520
  console.error(` Valid options: ${validBackends.join(', ')}`);
@@ -1547,10 +1632,30 @@ openclaw
1547
1632
  process.exit(1);
1548
1633
  }
1549
1634
  }
1635
+ else if (backend === 'keeper') {
1636
+ const { KeeperStore } = await import('../core/credentials/backends/keeper.js');
1637
+ if (!KeeperStore.isAvailable()) {
1638
+ console.error(' Keeper Commander (keeper) not found.');
1639
+ console.error(' Install: pip install keepercommander');
1640
+ console.error(' Then, once: keeper shell -> login -> this-device register -> this-device persistent-login on');
1641
+ process.exit(1);
1642
+ }
1643
+ if (!process.env['AQUAMAN_KEEPER_FOLDER_UID']) {
1644
+ console.error(' Keeper backend needs the folder that will hold aquaman records.');
1645
+ console.error(' Create one in Keeper, then: export AQUAMAN_KEEPER_FOLDER_UID=<folder UID>');
1646
+ console.error(' (find the UID with: keeper get "<folder name>" --format json)');
1647
+ process.exit(1);
1648
+ }
1649
+ }
1550
1650
  // 2. Run init internally (create dirs, config)
1551
1651
  ensureConfigDir();
1552
1652
  const config = getDefaultConfig();
1553
1653
  config.credentials.backend = backend;
1654
+ if (backend === 'keeper') {
1655
+ config.credentials.keeperFolderUid = process.env['AQUAMAN_KEEPER_FOLDER_UID'];
1656
+ if (process.env['AQUAMAN_KEEPER_CONFIG'])
1657
+ config.credentials.keeperConfigPath = process.env['AQUAMAN_KEEPER_CONFIG'];
1658
+ }
1554
1659
  fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
1555
1660
  // Create audit directory
1556
1661
  const auditDir = path.join(configDir, 'audit');
@@ -2007,6 +2112,12 @@ openclaw
2007
2112
  throw new Error('systemd-creds backend requires systemd >= 256 with --user support');
2008
2113
  }
2009
2114
  }
2115
+ if (config.credentials.backend === 'keeper') {
2116
+ const { KeeperStore } = await import('../core/credentials/backends/keeper.js');
2117
+ if (!KeeperStore.isAvailable()) {
2118
+ throw new Error('Keeper Commander (keeper) not found. Install: pip install keepercommander');
2119
+ }
2120
+ }
2010
2121
  if (config.credentials.backend === 'bitwarden') {
2011
2122
  const { BitwardenStore } = await import('../core/credentials/backends/bitwarden.js');
2012
2123
  if (!BitwardenStore.isAvailable()) {
@@ -2027,7 +2138,9 @@ openclaw
2027
2138
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2028
2139
  bitwardenFolder: config.credentials.bitwardenFolder,
2029
2140
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2030
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2141
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2142
+ keeperFolderUid: config.credentials.keeperFolderUid,
2143
+ keeperConfigPath: config.credentials.keeperConfigPath
2031
2144
  });
2032
2145
  // 3. Count credentials
2033
2146
  const creds = await store.list();
@@ -2582,7 +2695,9 @@ credentials
2582
2695
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2583
2696
  bitwardenFolder: config.credentials.bitwardenFolder,
2584
2697
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2585
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2698
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2699
+ keeperFolderUid: config.credentials.keeperFolderUid,
2700
+ keeperConfigPath: config.credentials.keeperConfigPath
2586
2701
  });
2587
2702
  }
2588
2703
  catch (error) {
@@ -2665,7 +2780,9 @@ credentials
2665
2780
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2666
2781
  bitwardenFolder: config.credentials.bitwardenFolder,
2667
2782
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2668
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2783
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2784
+ keeperFolderUid: config.credentials.keeperFolderUid,
2785
+ keeperConfigPath: config.credentials.keeperConfigPath
2669
2786
  });
2670
2787
  }
2671
2788
  catch {
@@ -2702,7 +2819,9 @@ credentials
2702
2819
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2703
2820
  bitwardenFolder: config.credentials.bitwardenFolder,
2704
2821
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2705
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2822
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2823
+ keeperFolderUid: config.credentials.keeperFolderUid,
2824
+ keeperConfigPath: config.credentials.keeperConfigPath
2706
2825
  });
2707
2826
  }
2708
2827
  catch {
@@ -2720,7 +2839,7 @@ credentials
2720
2839
  credentials
2721
2840
  .command('guide')
2722
2841
  .description('Show setup commands for seeding credentials based on your backend')
2723
- .option('--backend <backend>', 'Override backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
2842
+ .option('--backend <backend>', 'Override backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
2724
2843
  .option('--service <name>', 'Show commands for a single service only')
2725
2844
  .action(async (options) => {
2726
2845
  const config = loadConfig();
@@ -2881,6 +3000,151 @@ policy
2881
3000
  console.log(` \u2717 DENIED by rule: ${rule.method} ${rule.path} \u2192 ${rule.action}`);
2882
3001
  }
2883
3002
  });
3003
+ // ── aquaman openshell …: NVIDIA OpenShell external credential driver (v0.17.0+) ──
3004
+ const openshell = program
3005
+ .command('openshell')
3006
+ .description('NVIDIA OpenShell integration: aquaman as the gateway credential driver');
3007
+ /** Edit config.yaml `openshell.driver` in the raw file so env overrides are never persisted. */
3008
+ function updateOpenShellDriverConfig(enabled, socketPath) {
3009
+ ensureConfigDir();
3010
+ const configPath = getConfigPath();
3011
+ let raw = {};
3012
+ if (fs.existsSync(configPath)) {
3013
+ const parsed = yamlParse(fs.readFileSync(configPath, 'utf-8'));
3014
+ if (parsed && typeof parsed === 'object')
3015
+ raw = parsed;
3016
+ }
3017
+ const current = raw.openshell?.driver && typeof raw.openshell.driver === 'object' ? raw.openshell.driver : {};
3018
+ raw.openshell = { ...(raw.openshell && typeof raw.openshell === 'object' ? raw.openshell : {}), driver: { ...current, enabled } };
3019
+ if (socketPath)
3020
+ raw.openshell.driver.socketPath = socketPath;
3021
+ fs.writeFileSync(configPath, yamlStringify(raw), { encoding: 'utf-8', mode: 0o600 });
3022
+ }
3023
+ openshell
3024
+ .command('setup')
3025
+ .description('Serve the OpenShell credential driver from `aquaman daemon` and print the gateway config')
3026
+ .option('--socket <path>', 'Absolute Unix socket path for the driver (default: <configDir>/openshell.sock)')
3027
+ .option('--disable', 'Turn the driver off again')
3028
+ .action(async (opts) => {
3029
+ const { grpcAvailable, gatewayConfigSnippet, openshellDriverSocketPath, MAX_SOCKET_PATH } = await import('../openshell/integration.js');
3030
+ const { GRPC_INSTALL_HINT } = await import('../openshell/grpc-server.js');
3031
+ if (opts.disable) {
3032
+ updateOpenShellDriverConfig(false);
3033
+ console.log('OpenShell credential driver disabled. Restart the daemon: aquaman stop && aquaman daemon');
3034
+ console.log('Remove the [openshell.credential_drivers.aquaman] table from your gateway config too.');
3035
+ return;
3036
+ }
3037
+ if (opts.socket && !path.isAbsolute(opts.socket)) {
3038
+ console.error('--socket must be an absolute path (the gateway config needs one).');
3039
+ process.exit(1);
3040
+ }
3041
+ updateOpenShellDriverConfig(true, opts.socket);
3042
+ const socketPath = openshellDriverSocketPath(loadConfig());
3043
+ console.log(`OpenShell credential driver enabled in ${getConfigPath()}`);
3044
+ if (Buffer.byteLength(socketPath) > MAX_SOCKET_PATH) {
3045
+ console.error(` ⚠ ${socketPath} is longer than the ${MAX_SOCKET_PATH}-byte Unix socket limit. Rerun with --socket <shorter path>.`);
3046
+ }
3047
+ if (!(await grpcAvailable())) {
3048
+ console.log(`\n The driver needs two optional packages: ${aqua(GRPC_INSTALL_HINT)}`);
3049
+ }
3050
+ console.log('\n1. Add this to your OpenShell gateway config (TOML):\n');
3051
+ console.log(gatewayConfigSnippet(socketPath).split('\n').map((l) => ` ${l}`).join('\n'));
3052
+ console.log('\n2. Restart aquaman, then the gateway:');
3053
+ console.log(' aquaman stop; aquaman daemon');
3054
+ console.log('\n3. Create providers by reference. The secret stays in your vault; nothing is copied:');
3055
+ console.log(` ${aqua('aquaman broker allow aquaman://anthropic/api_key')}`);
3056
+ console.log(` openshell provider create --name claude --type anthropic --credential ANTHROPIC_API_KEY=aquaman://anthropic/api_key`);
3057
+ console.log('\n A plain value (--credential KEY=sk-...) is stored in your vault under the "openshell" service instead.');
3058
+ console.log(`\nCheck everything with: ${aqua('aquaman openshell doctor')}`);
3059
+ });
3060
+ openshell
3061
+ .command('doctor')
3062
+ .description('Check the OpenShell credential driver: packages, config, socket, declared refs')
3063
+ .option('--gateway-config <path>', 'Also check an OpenShell gateway TOML file points at this driver')
3064
+ .action(async (opts) => {
3065
+ const net = await import('node:net');
3066
+ const { grpcAvailable, openshellDriverSocketPath, MAX_SOCKET_PATH } = await import('../openshell/integration.js');
3067
+ const { GRPC_INSTALL_HINT } = await import('../openshell/grpc-server.js');
3068
+ const config = loadConfig();
3069
+ const socketPath = openshellDriverSocketPath(config);
3070
+ let failed = 0;
3071
+ const pass = (m) => console.log(` ✓ ${m}`);
3072
+ const fail = (m, fix) => { failed++; console.log(` ✗ ${m}`); if (fix)
3073
+ console.log(` → ${fix}`); };
3074
+ console.log(`\n ${aqua('OpenShell credential driver')}\n`);
3075
+ if (await grpcAvailable())
3076
+ pass('gRPC packages installed (@grpc/grpc-js, @grpc/proto-loader)');
3077
+ else
3078
+ fail('gRPC packages missing', GRPC_INSTALL_HINT);
3079
+ if (config.openshell?.driver?.enabled)
3080
+ pass('Driver enabled in config.yaml');
3081
+ else
3082
+ fail('Driver not enabled', 'aquaman openshell setup');
3083
+ if (Buffer.byteLength(socketPath) <= MAX_SOCKET_PATH)
3084
+ pass(`Socket path ${socketPath}`);
3085
+ else
3086
+ fail(`Socket path over ${MAX_SOCKET_PATH} bytes: ${socketPath}`, 'aquaman openshell setup --socket <shorter absolute path>');
3087
+ let isSocket = false;
3088
+ try {
3089
+ isSocket = fs.statSync(socketPath).isSocket();
3090
+ }
3091
+ catch { /* absent */ }
3092
+ if (!isSocket) {
3093
+ fail('Driver socket not present (daemon not running, or started before setup)', 'aquaman stop; aquaman daemon');
3094
+ }
3095
+ else {
3096
+ const mode = fs.statSync(socketPath).mode & 0o777;
3097
+ if (mode === 0o600)
3098
+ pass('Socket is owner-only (0600)');
3099
+ else
3100
+ fail(`Socket mode is ${mode.toString(8)}, expected 600`, 'aquaman stop; aquaman daemon');
3101
+ const reachable = await new Promise((resolve) => {
3102
+ const c = net.connect(socketPath, () => { c.end(); resolve(true); });
3103
+ c.on('error', () => resolve(false));
3104
+ setTimeout(() => { c.destroy(); resolve(false); }, 2000);
3105
+ });
3106
+ if (reachable)
3107
+ pass('Driver socket accepts connections');
3108
+ else
3109
+ fail('Driver socket does not accept connections', 'aquaman stop; aquaman daemon');
3110
+ }
3111
+ const scope = daemonBrokerScope(config);
3112
+ if (!scope) {
3113
+ fail('Broker disabled (broker.enabled: false): references cannot resolve', 'set broker.enabled: true in config.yaml');
3114
+ }
3115
+ else {
3116
+ const n = scope.declared().length;
3117
+ if (n > 0)
3118
+ pass(`${n} declared ref${n === 1 ? '' : 's'} usable by reference (aquaman broker list)`);
3119
+ else
3120
+ console.log(' • No declared refs yet: reference-mode providers will be refused until you run aquaman broker allow <ref>');
3121
+ }
3122
+ if (opts.gatewayConfig) {
3123
+ let toml = '';
3124
+ try {
3125
+ toml = fs.readFileSync(opts.gatewayConfig, 'utf-8');
3126
+ }
3127
+ catch {
3128
+ fail(`Cannot read ${opts.gatewayConfig}`);
3129
+ }
3130
+ if (toml) {
3131
+ const listsDriver = /credential_drivers\s*=\s*\[[^\]]*"aquaman"[^\]]*\]/.test(toml);
3132
+ const table = /\[openshell\.credential_drivers\.aquaman\]([\s\S]*?)(?=\n\[|$)/.exec(toml)?.[1] ?? '';
3133
+ const uds = /transport\s*=\s*"uds"/.test(table);
3134
+ const sock = /socket_path\s*=\s*"([^"]*)"/.exec(table)?.[1];
3135
+ if (listsDriver && uds && sock === socketPath)
3136
+ pass(`Gateway config ${opts.gatewayConfig} points at this driver`);
3137
+ else
3138
+ fail(`Gateway config ${opts.gatewayConfig} does not point at this driver`, 'paste the snippet from: aquaman openshell setup');
3139
+ }
3140
+ }
3141
+ console.log('');
3142
+ if (failed > 0) {
3143
+ console.log(` ${failed} check${failed === 1 ? '' : 's'} failed.\n`);
3144
+ process.exit(1);
3145
+ }
3146
+ console.log(' All checks passed.\n');
3147
+ });
2884
3148
  // Credential broker scope (v0.15.0+)
2885
3149
  const broker = program
2886
3150
  .command('broker')
@@ -2923,6 +3187,63 @@ broker
2923
3187
  .action((refs) => {
2924
3188
  updateBrokerAllowedRefs(refs, 'revoke');
2925
3189
  });
3190
+ // v0.16.0: fills the "run a command, read the secret from stdout" slot every
3191
+ // host now has (sbx secret set --command, Codex model_providers.*.auth.command,
3192
+ // Claude Code apiKeyHelper, OpenClaw exec providers, Hermes command sources).
3193
+ // This materializes the value into the CALLING process, so it is not process
3194
+ // isolation; it is scope-checked and audited because it goes through the daemon.
3195
+ program
3196
+ .command('get')
3197
+ .description('Print a declared credential to stdout, via the running daemon (for host credential-helper commands)')
3198
+ .argument('<ref>', 'aquaman://service/key, declared with `aquaman broker allow` or in projects.yaml')
3199
+ .option('--show', 'Allow printing to an interactive terminal')
3200
+ .option('--timeout <ms>', 'How long to wait for the daemon (vaults that prompt may need longer)', '15000')
3201
+ .addHelpText('after', `
3202
+ Examples:
3203
+ sbx secret set github --command "$(command -v node) $(command -v aquaman) get aquaman://github/token"
3204
+ Codex config.toml: [model_providers.openai.auth] command = "aquaman" args = ["get", "aquaman://openai/api_key"]
3205
+ Claude Code: "apiKeyHelper": "aquaman get aquaman://anthropic/api_key"
3206
+
3207
+ The value lands in whichever process runs this command. Every read is checked
3208
+ against the broker scope and written to the audit log.`)
3209
+ .action(async (ref, opts) => {
3210
+ const parsed = parseAquamanRef(ref);
3211
+ if (!parsed) {
3212
+ console.error(`Invalid ref: ${ref}`);
3213
+ console.error('Refs look like aquaman://<service>/<key>, e.g. aquaman://github/token');
3214
+ process.exit(1);
3215
+ }
3216
+ const timeoutMs = Number(opts.timeout);
3217
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
3218
+ console.error(`Invalid --timeout: ${opts.timeout} (milliseconds, a positive integer)`);
3219
+ process.exit(1);
3220
+ }
3221
+ // Refuse before resolving, so an accidental run at a prompt neither
3222
+ // prints the secret into scrollback nor spends an audited read.
3223
+ if (process.stdout.isTTY && !opts.show) {
3224
+ console.error('Refusing to print a credential to a terminal.');
3225
+ console.error('This command is meant to be run by another program. To print it anyway, pass --show.');
3226
+ process.exit(1);
3227
+ }
3228
+ const result = await brokerResolve(path.join(getConfigDir(), 'proxy.sock'), parsed.service, parsed.key, timeoutMs);
3229
+ if ('value' in result) {
3230
+ // No trailing newline when piped: credential-helper consumers differ on
3231
+ // whether they trim, and a stray "\n" in an auth header breaks requests.
3232
+ process.stdout.write(process.stdout.isTTY ? `${result.value}\n` : result.value);
3233
+ return;
3234
+ }
3235
+ console.error(`Error: ${result.error}`);
3236
+ if (result.code === 'broker_ref_not_declared') {
3237
+ console.error(`Fix: aquaman broker allow ${ref}`);
3238
+ }
3239
+ else if (result.code === 'broker_disabled') {
3240
+ 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).');
3241
+ }
3242
+ else if (result.fix) {
3243
+ console.error(`Fix: ${result.fix}`);
3244
+ }
3245
+ process.exit(1);
3246
+ });
2926
3247
  // Migration commands
2927
3248
  openclaw
2928
3249
  .command('migrate')
@@ -3356,7 +3677,9 @@ openclaw
3356
3677
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
3357
3678
  bitwardenFolder: config.credentials.bitwardenFolder,
3358
3679
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
3359
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
3680
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
3681
+ keeperFolderUid: config.credentials.keeperFolderUid,
3682
+ keeperConfigPath: config.credentials.keeperConfigPath
3360
3683
  });
3361
3684
  const creds = await store.list();
3362
3685
  console.log(`\nStored credentials: ${creds.length}`);