aquaman-proxy 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,8 @@ 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
+
100
102
  ## Documentation
101
103
 
102
104
  - **[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) {
@@ -389,7 +442,7 @@ program
389
442
  program
390
443
  .command('setup')
391
444
  .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)')
445
+ .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
393
446
  .option('--no-policy', 'Skip request policy preset configuration')
394
447
  .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
395
448
  .action(async (options) => {
@@ -614,7 +667,7 @@ async function runVaultSetup(opts) {
614
667
  }
615
668
  }
616
669
  }
617
- const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
670
+ const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden', 'keeper'];
618
671
  if (!validBackends.includes(backend)) {
619
672
  console.error(` Invalid backend: ${backend}. Valid: ${validBackends.join(', ')}`);
620
673
  process.exit(1);
@@ -630,6 +683,9 @@ async function runVaultSetup(opts) {
630
683
  config = loadConfig();
631
684
  }
632
685
  config.credentials.backend = backend;
686
+ if (backend === 'keeper' && process.env['AQUAMAN_KEEPER_FOLDER_UID']) {
687
+ config.credentials.keeperFolderUid = process.env['AQUAMAN_KEEPER_FOLDER_UID'];
688
+ }
633
689
  fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
634
690
  const auditDir = path.join(configDir, 'audit');
635
691
  fs.mkdirSync(auditDir, { recursive: true, mode: 0o700 });
@@ -785,7 +841,8 @@ openclaw
785
841
  onRequest: (info) => {
786
842
  auditLogger.logCredentialAccess('system', 'system', {
787
843
  service: info.service,
788
- operation: 'use',
844
+ // A broker resolve hands the value out; everything else injects it.
845
+ operation: info.method === 'BROKER' ? 'read' : 'use',
789
846
  success: !info.error,
790
847
  error: info.error
791
848
  });
@@ -931,7 +988,8 @@ program
931
988
  onRequest: (info) => {
932
989
  auditLogger.logCredentialAccess('system', 'system', {
933
990
  service: info.service,
934
- operation: 'use',
991
+ // A broker resolve hands the value out; everything else injects it.
992
+ operation: info.method === 'BROKER' ? 'read' : 'use',
935
993
  success: !info.error,
936
994
  error: info.error
937
995
  });
@@ -1004,7 +1062,8 @@ openclaw
1004
1062
  onRequest: (info) => {
1005
1063
  auditLogger.logCredentialAccess('system', 'system', {
1006
1064
  service: info.service,
1007
- operation: 'use',
1065
+ // A broker resolve hands the value out; everything else injects it.
1066
+ operation: info.method === 'BROKER' ? 'read' : 'use',
1008
1067
  success: !info.error,
1009
1068
  error: info.error
1010
1069
  });
@@ -1382,7 +1441,7 @@ program
1382
1441
  openclaw
1383
1442
  .command('setup')
1384
1443
  .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)')
1444
+ .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
1386
1445
  .option('--no-openclaw', 'Skip OpenClaw plugin installation step (run vault setup only)')
1387
1446
  .option('--no-policy', 'Skip request policy preset configuration')
1388
1447
  .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
@@ -1429,7 +1488,7 @@ openclaw
1429
1488
  }
1430
1489
  }
1431
1490
  // Validate backend
1432
- const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
1491
+ const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden', 'keeper'];
1433
1492
  if (!validBackends.includes(backend)) {
1434
1493
  console.error(` Invalid backend: ${backend}`);
1435
1494
  console.error(` Valid options: ${validBackends.join(', ')}`);
@@ -1547,10 +1606,30 @@ openclaw
1547
1606
  process.exit(1);
1548
1607
  }
1549
1608
  }
1609
+ else if (backend === 'keeper') {
1610
+ const { KeeperStore } = await import('../core/credentials/backends/keeper.js');
1611
+ if (!KeeperStore.isAvailable()) {
1612
+ console.error(' Keeper Commander (keeper) not found.');
1613
+ console.error(' Install: pip install keepercommander');
1614
+ console.error(' Then, once: keeper shell -> login -> this-device register -> this-device persistent-login on');
1615
+ process.exit(1);
1616
+ }
1617
+ if (!process.env['AQUAMAN_KEEPER_FOLDER_UID']) {
1618
+ console.error(' Keeper backend needs the folder that will hold aquaman records.');
1619
+ console.error(' Create one in Keeper, then: export AQUAMAN_KEEPER_FOLDER_UID=<folder UID>');
1620
+ console.error(' (find the UID with: keeper get "<folder name>" --format json)');
1621
+ process.exit(1);
1622
+ }
1623
+ }
1550
1624
  // 2. Run init internally (create dirs, config)
1551
1625
  ensureConfigDir();
1552
1626
  const config = getDefaultConfig();
1553
1627
  config.credentials.backend = backend;
1628
+ if (backend === 'keeper') {
1629
+ config.credentials.keeperFolderUid = process.env['AQUAMAN_KEEPER_FOLDER_UID'];
1630
+ if (process.env['AQUAMAN_KEEPER_CONFIG'])
1631
+ config.credentials.keeperConfigPath = process.env['AQUAMAN_KEEPER_CONFIG'];
1632
+ }
1554
1633
  fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
1555
1634
  // Create audit directory
1556
1635
  const auditDir = path.join(configDir, 'audit');
@@ -2007,6 +2086,12 @@ openclaw
2007
2086
  throw new Error('systemd-creds backend requires systemd >= 256 with --user support');
2008
2087
  }
2009
2088
  }
2089
+ if (config.credentials.backend === 'keeper') {
2090
+ const { KeeperStore } = await import('../core/credentials/backends/keeper.js');
2091
+ if (!KeeperStore.isAvailable()) {
2092
+ throw new Error('Keeper Commander (keeper) not found. Install: pip install keepercommander');
2093
+ }
2094
+ }
2010
2095
  if (config.credentials.backend === 'bitwarden') {
2011
2096
  const { BitwardenStore } = await import('../core/credentials/backends/bitwarden.js');
2012
2097
  if (!BitwardenStore.isAvailable()) {
@@ -2027,7 +2112,9 @@ openclaw
2027
2112
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2028
2113
  bitwardenFolder: config.credentials.bitwardenFolder,
2029
2114
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2030
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2115
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2116
+ keeperFolderUid: config.credentials.keeperFolderUid,
2117
+ keeperConfigPath: config.credentials.keeperConfigPath
2031
2118
  });
2032
2119
  // 3. Count credentials
2033
2120
  const creds = await store.list();
@@ -2582,7 +2669,9 @@ credentials
2582
2669
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2583
2670
  bitwardenFolder: config.credentials.bitwardenFolder,
2584
2671
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2585
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2672
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2673
+ keeperFolderUid: config.credentials.keeperFolderUid,
2674
+ keeperConfigPath: config.credentials.keeperConfigPath
2586
2675
  });
2587
2676
  }
2588
2677
  catch (error) {
@@ -2665,7 +2754,9 @@ credentials
2665
2754
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2666
2755
  bitwardenFolder: config.credentials.bitwardenFolder,
2667
2756
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2668
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2757
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2758
+ keeperFolderUid: config.credentials.keeperFolderUid,
2759
+ keeperConfigPath: config.credentials.keeperConfigPath
2669
2760
  });
2670
2761
  }
2671
2762
  catch {
@@ -2702,7 +2793,9 @@ credentials
2702
2793
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
2703
2794
  bitwardenFolder: config.credentials.bitwardenFolder,
2704
2795
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
2705
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
2796
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
2797
+ keeperFolderUid: config.credentials.keeperFolderUid,
2798
+ keeperConfigPath: config.credentials.keeperConfigPath
2706
2799
  });
2707
2800
  }
2708
2801
  catch {
@@ -2720,7 +2813,7 @@ credentials
2720
2813
  credentials
2721
2814
  .command('guide')
2722
2815
  .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)')
2816
+ .option('--backend <backend>', 'Override backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden, keeper)')
2724
2817
  .option('--service <name>', 'Show commands for a single service only')
2725
2818
  .action(async (options) => {
2726
2819
  const config = loadConfig();
@@ -2923,6 +3016,63 @@ broker
2923
3016
  .action((refs) => {
2924
3017
  updateBrokerAllowedRefs(refs, 'revoke');
2925
3018
  });
3019
+ // v0.16.0: fills the "run a command, read the secret from stdout" slot every
3020
+ // host now has (sbx secret set --command, Codex model_providers.*.auth.command,
3021
+ // Claude Code apiKeyHelper, OpenClaw exec providers, Hermes command sources).
3022
+ // This materializes the value into the CALLING process, so it is not process
3023
+ // isolation; it is scope-checked and audited because it goes through the daemon.
3024
+ program
3025
+ .command('get')
3026
+ .description('Print a declared credential to stdout, via the running daemon (for host credential-helper commands)')
3027
+ .argument('<ref>', 'aquaman://service/key, declared with `aquaman broker allow` or in projects.yaml')
3028
+ .option('--show', 'Allow printing to an interactive terminal')
3029
+ .option('--timeout <ms>', 'How long to wait for the daemon (vaults that prompt may need longer)', '15000')
3030
+ .addHelpText('after', `
3031
+ Examples:
3032
+ sbx secret set github --command "$(command -v node) $(command -v aquaman) get aquaman://github/token"
3033
+ Codex config.toml: [model_providers.openai.auth] command = "aquaman" args = ["get", "aquaman://openai/api_key"]
3034
+ Claude Code: "apiKeyHelper": "aquaman get aquaman://anthropic/api_key"
3035
+
3036
+ The value lands in whichever process runs this command. Every read is checked
3037
+ against the broker scope and written to the audit log.`)
3038
+ .action(async (ref, opts) => {
3039
+ const parsed = parseAquamanRef(ref);
3040
+ if (!parsed) {
3041
+ console.error(`Invalid ref: ${ref}`);
3042
+ console.error('Refs look like aquaman://<service>/<key>, e.g. aquaman://github/token');
3043
+ process.exit(1);
3044
+ }
3045
+ const timeoutMs = Number(opts.timeout);
3046
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
3047
+ console.error(`Invalid --timeout: ${opts.timeout} (milliseconds, a positive integer)`);
3048
+ process.exit(1);
3049
+ }
3050
+ // Refuse before resolving, so an accidental run at a prompt neither
3051
+ // prints the secret into scrollback nor spends an audited read.
3052
+ if (process.stdout.isTTY && !opts.show) {
3053
+ console.error('Refusing to print a credential to a terminal.');
3054
+ console.error('This command is meant to be run by another program. To print it anyway, pass --show.');
3055
+ process.exit(1);
3056
+ }
3057
+ const result = await brokerResolve(path.join(getConfigDir(), 'proxy.sock'), parsed.service, parsed.key, timeoutMs);
3058
+ if ('value' in result) {
3059
+ // No trailing newline when piped: credential-helper consumers differ on
3060
+ // whether they trim, and a stray "\n" in an auth header breaks requests.
3061
+ process.stdout.write(process.stdout.isTTY ? `${result.value}\n` : result.value);
3062
+ return;
3063
+ }
3064
+ console.error(`Error: ${result.error}`);
3065
+ if (result.code === 'broker_ref_not_declared') {
3066
+ console.error(`Fix: aquaman broker allow ${ref}`);
3067
+ }
3068
+ else if (result.code === 'broker_disabled') {
3069
+ console.error('Fix: the proxy on this socket does not serve the broker. If the OpenClaw plugin started it, run `aquaman daemon` instead (the last proxy started owns the socket).');
3070
+ }
3071
+ else if (result.fix) {
3072
+ console.error(`Fix: ${result.fix}`);
3073
+ }
3074
+ process.exit(1);
3075
+ });
2926
3076
  // Migration commands
2927
3077
  openclaw
2928
3078
  .command('migrate')
@@ -3356,7 +3506,9 @@ openclaw
3356
3506
  keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
3357
3507
  bitwardenFolder: config.credentials.bitwardenFolder,
3358
3508
  bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
3359
- bitwardenCollectionId: config.credentials.bitwardenCollectionId
3509
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId,
3510
+ keeperFolderUid: config.credentials.keeperFolderUid,
3511
+ keeperConfigPath: config.credentials.keeperConfigPath
3360
3512
  });
3361
3513
  const creds = await store.list();
3362
3514
  console.log(`\nStored credentials: ${creds.length}`);