aquaman-proxy 0.11.5 → 0.12.1

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 (35) hide show
  1. package/README.md +49 -56
  2. package/dist/cli/index.js +608 -59
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/credentials/backends/onepassword.d.ts +2 -0
  5. package/dist/core/credentials/backends/onepassword.d.ts.map +1 -1
  6. package/dist/core/credentials/backends/onepassword.js +75 -33
  7. package/dist/core/credentials/backends/onepassword.js.map +1 -1
  8. package/dist/core/credentials/index.d.ts +1 -1
  9. package/dist/core/credentials/index.d.ts.map +1 -1
  10. package/dist/core/credentials/index.js +1 -1
  11. package/dist/core/credentials/index.js.map +1 -1
  12. package/dist/core/credentials/store.d.ts.map +1 -1
  13. package/dist/core/credentials/store.js +2 -4
  14. package/dist/core/credentials/store.js.map +1 -1
  15. package/dist/core/index.d.ts +2 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +3 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/core/redactor/index.d.ts +96 -0
  20. package/dist/core/redactor/index.d.ts.map +1 -0
  21. package/dist/core/redactor/index.js +230 -0
  22. package/dist/core/redactor/index.js.map +1 -0
  23. package/dist/daemon.d.ts +20 -0
  24. package/dist/daemon.d.ts.map +1 -1
  25. package/dist/daemon.js +147 -0
  26. package/dist/daemon.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/openclaw/integration.d.ts +13 -0
  32. package/dist/openclaw/integration.d.ts.map +1 -1
  33. package/dist/openclaw/integration.js +30 -0
  34. package/dist/openclaw/integration.js.map +1 -1
  35. package/package.json +4 -4
package/dist/cli/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - Hash-chained audit logs: Tamper-evident logging
9
9
  * - Custom service registry: YAML-based service config
10
10
  */
11
- import { Command } from 'commander';
11
+ 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';
@@ -16,7 +16,7 @@ import { loadConfig, getConfigDir, ensureConfigDir, getDefaultConfig, expandPath
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 } from '../openclaw/integration.js';
19
+ import { createOpenClawIntegration, authProfilesAreSqliteOnly } from '../openclaw/integration.js';
20
20
  import { loadPolicyFromConfig, validatePolicyConfig, getDefaultPolicyPresets, matchPolicy } from '../request-policy.js';
21
21
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
22
22
  // Read version from package.json (single source of truth)
@@ -108,9 +108,8 @@ async function promptSecretInput(prompt) {
108
108
  const program = new Command();
109
109
  program
110
110
  .name('aquaman')
111
- .description('Credential isolation layer for OpenClaw - keeps API keys outside the agent process')
111
+ .description('Credential isolation for AI agents \u2014 vault for the proxy core, OpenClaw plugin path, coding-agent adapter path')
112
112
  .version(VERSION)
113
- .addHelpText('before', `\n\u{1F531}\u{1F99E} Aquaman ${aqua(VERSION)} \u2014 Credential isolation for OpenClaw\n`)
114
113
  .configureHelp({
115
114
  subcommandTerm(cmd) {
116
115
  const args = cmd.registeredArguments
@@ -119,11 +118,443 @@ program
119
118
  return arg.required ? `<${n}>` : `[${n}]`;
120
119
  })
121
120
  .join(' ');
122
- return aqua(cmd.name()) + (cmd.options.length ? ' [options]' : '') + (args ? ' ' + args : '');
121
+ // Command names render in default color; only section headings
122
+ // (rendered by our custom formatHelp below) get the aqua treatment.
123
+ return cmd.name() + (cmd.options.length ? ' [options]' : '') + (args ? ' ' + args : '');
124
+ },
125
+ // Custom grouped help layout for the root program. Subcommand helps
126
+ // (e.g. `aquaman openclaw --help`) use Commander's default formatter.
127
+ formatHelp(cmd, helper) {
128
+ // Only customize the ROOT command's help. Nested subcommands keep the
129
+ // default layout \u2014 call the un-overridden formatter directly to avoid
130
+ // recursing into this override.
131
+ if (cmd !== program)
132
+ return Help.prototype.formatHelp.call(helper, cmd, helper);
133
+ const lines = [];
134
+ lines.push('');
135
+ lines.push(` \u{1F531} ${aqua('Aquaman')} ${VERSION} \u2014 Credential isolation for AI agents`);
136
+ lines.push('');
137
+ lines.push(`Usage: ${helper.commandUsage(cmd)}`);
138
+ lines.push('');
139
+ lines.push(cmd.description());
140
+ lines.push('');
141
+ // Options
142
+ const opts = helper.visibleOptions(cmd);
143
+ if (opts.length) {
144
+ lines.push('Options:');
145
+ for (const opt of opts) {
146
+ lines.push(` ${helper.optionTerm(opt).padEnd(20)} ${helper.optionDescription(opt)}`);
147
+ }
148
+ lines.push('');
149
+ }
150
+ const all = helper.visibleCommands(cmd);
151
+ const byName = new Map(all.map((c) => [c.name(), c]));
152
+ const renderRow = (term, desc) => ` ${term.padEnd(30)} ${desc}`;
153
+ const renderCmd = (c) => renderRow(c.name(), c.description() || '');
154
+ // --- Vault & core (agent-agnostic) ---
155
+ lines.push(aqua('Vault & core (agent-agnostic)'));
156
+ const vaultCore = ['setup', 'doctor', 'status', 'daemon', 'stop', 'init'];
157
+ for (const name of vaultCore) {
158
+ const c = byName.get(name);
159
+ if (c)
160
+ lines.push(renderCmd(c));
161
+ }
162
+ lines.push('');
163
+ lines.push(aqua('Vault management'));
164
+ for (const name of ['credentials', 'audit', 'services', 'policy']) {
165
+ const c = byName.get(name);
166
+ if (c)
167
+ lines.push(renderCmd(c));
168
+ }
169
+ lines.push('');
170
+ // --- OpenClaw namespace (nested) ---
171
+ const oc = byName.get('openclaw');
172
+ if (oc) {
173
+ lines.push(aqua('OpenClaw Gateway integration'));
174
+ // Ordered for readability (setup/doctor/status first, then lifecycle).
175
+ const ocOrder = ['setup', 'doctor', 'status', 'start', 'configure', 'migrate'];
176
+ const ocSubs = helper.visibleCommands(oc).filter((s) => s.name() !== 'help');
177
+ const ocSorted = [
178
+ ...ocOrder.map((n) => ocSubs.find((s) => s.name() === n)).filter(Boolean),
179
+ ...ocSubs.filter((s) => !ocOrder.includes(s.name())),
180
+ ];
181
+ for (const sub of ocSorted) {
182
+ lines.push(renderRow(`openclaw ${sub.name()}`, sub.description() || ''));
183
+ }
184
+ lines.push('');
185
+ }
186
+ // --- Coder namespace (shim \u2014 list documented subcommands) ---
187
+ if (byName.has('coder')) {
188
+ lines.push(aqua('AI coding-agent integration (Claude Code today)'));
189
+ const coderSubs = [
190
+ ['coder setup <agent>', 'Install hooks for an agent (claude-code today)'],
191
+ ['coder doctor', 'Deep diagnostic \u2014 projects, broker, per-project vault'],
192
+ ['coder status', 'Configured projects + hook wiring + broker activity'],
193
+ ['coder project list/add/remove', 'Manage ~/.aquaman/projects.yaml'],
194
+ ['coder get <ref>', 'Resolve an aquaman://service/key reference'],
195
+ ['coder exec <cmd>', 'Run command with project env injected + output redacted'],
196
+ ];
197
+ for (const [term, desc] of coderSubs) {
198
+ lines.push(renderRow(term, desc));
199
+ }
200
+ lines.push(` ${'(delegates to the aquaman-coder binary; install: npm install -g aquaman-coder)'}`);
201
+ lines.push('');
202
+ }
203
+ // --- Other ---
204
+ const helpCmd = byName.get('help');
205
+ if (helpCmd) {
206
+ lines.push(aqua('Other'));
207
+ lines.push(renderCmd(helpCmd));
208
+ lines.push('');
209
+ }
210
+ return lines.join('\n');
211
+ }
212
+ });
213
+ // ============================================================================
214
+ // Top-level commands (vault-only, agent-agnostic)
215
+ // ============================================================================
216
+ //
217
+ // `aquaman setup`, `aquaman doctor`, `aquaman status` cover the proxy + vault
218
+ // surface only. For full bundles, use the namespaced versions:
219
+ // aquaman openclaw setup full OpenClaw bundle
220
+ // aquaman coder setup ... coding-agent adapter
221
+ // ============================================================================
222
+ // aquaman setup \u2014 vault-only minimal setup wizard.
223
+ program
224
+ .command('setup')
225
+ .description('Vault-only setup wizard \u2014 backend + credentials (use `aquaman openclaw setup` or `aquaman coder setup` for full bundles)')
226
+ .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
227
+ .option('--no-policy', 'Skip request policy preset configuration')
228
+ .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
229
+ .action(async (options) => {
230
+ await runVaultSetup({
231
+ backend: options.backend,
232
+ policy: options.policy !== false,
233
+ nonInteractive: !!options.nonInteractive,
234
+ });
235
+ console.log('');
236
+ console.log(' Next steps:');
237
+ console.log(' \u2022 OpenClaw Gateway user? ' + aqua('aquaman openclaw setup'));
238
+ console.log(' \u2022 Claude Code / Codex / etc? ' + aqua('aquaman coder setup claude-code') + ' (requires aquaman-coder)');
239
+ console.log('');
240
+ });
241
+ // aquaman doctor \u2014 overview with persona-aware soft upsells.
242
+ program
243
+ .command('doctor')
244
+ .description('Overview health check (vault + integration summaries with soft upsells)')
245
+ .action(async () => {
246
+ const os = await import('node:os');
247
+ const configDir = getConfigDir();
248
+ const configPath = path.join(configDir, 'config.yaml');
249
+ const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
250
+ let issues = 0;
251
+ console.log('');
252
+ console.log(` \u{1F531} Aquaman ${VERSION} \u2014 health check`);
253
+ console.log('');
254
+ console.log(` ${aqua('Vault')}`);
255
+ // Vault \u2014 config file
256
+ if (!fs.existsSync(configPath)) {
257
+ console.log(` \u2717 Config missing (${configPath})`);
258
+ console.log(' \u2192 Run: aquaman setup');
259
+ issues++;
260
+ }
261
+ else {
262
+ // Vault \u2014 backend + creds
263
+ try {
264
+ const config = loadConfig();
265
+ const store = await createCredentialStore({
266
+ backend: config.credentials.backend,
267
+ encryptionPassword: config.credentials.encryptionPassword,
268
+ vaultAddress: config.credentials.vaultAddress,
269
+ vaultToken: config.credentials.vaultToken,
270
+ onePasswordVault: config.credentials.onePasswordVault,
271
+ onePasswordAccount: config.credentials.onePasswordAccount,
272
+ keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
273
+ keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
274
+ bitwardenFolder: config.credentials.bitwardenFolder,
275
+ bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
276
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId
277
+ });
278
+ const creds = await store.list();
279
+ console.log(` \u2713 ${config.credentials.backend} backend (${creds.length} credential${creds.length !== 1 ? 's' : ''})`);
280
+ }
281
+ catch (err) {
282
+ console.log(` \u2717 Backend not accessible: ${err.message}`);
283
+ console.log(' \u2192 Run: aquaman setup');
284
+ issues++;
285
+ }
286
+ // Vault \u2014 proxy running
287
+ const sockPath = path.join(configDir, 'proxy.sock');
288
+ try {
289
+ await new Promise((resolve, reject) => {
290
+ const req = http.request({ socketPath: sockPath, path: '/_health', method: 'GET' }, (res) => {
291
+ res.resume();
292
+ res.on('end', () => res.statusCode === 200 ? resolve() : reject(new Error(`HTTP ${res.statusCode}`)));
293
+ });
294
+ req.on('error', reject);
295
+ req.end();
296
+ });
297
+ console.log(` \u2713 Proxy running on socket`);
298
+ }
299
+ catch {
300
+ console.log(` \u2717 Proxy not running`);
301
+ console.log(' \u2192 Run: aquaman daemon &');
302
+ issues++;
303
+ }
304
+ }
305
+ // OpenClaw integration
306
+ let openclawDetected = false;
307
+ try {
308
+ const { execSync } = await import('node:child_process');
309
+ execSync('which openclaw', { stdio: 'pipe' });
310
+ openclawDetected = true;
311
+ }
312
+ catch { /* */ }
313
+ if (!openclawDetected)
314
+ openclawDetected = fs.existsSync(openclawStateDir);
315
+ console.log('');
316
+ console.log(` ${aqua('OpenClaw integration')}`);
317
+ if (!openclawDetected) {
318
+ console.log(` \u2022 not detected (skipping \u2014 only relevant if you run an OpenClaw Gateway)`);
319
+ }
320
+ else {
321
+ const pluginInstalled = fs.existsSync(path.join(openclawStateDir, 'extensions', 'aquaman-plugin'));
322
+ if (pluginInstalled) {
323
+ console.log(` \u2713 plugin installed (deep: ${aqua('aquaman openclaw doctor')})`);
324
+ }
325
+ else {
326
+ console.log(` \u2717 plugin not installed`);
327
+ console.log(' \u2192 Run: aquaman openclaw setup');
328
+ issues++;
329
+ }
330
+ }
331
+ // Coder integration
332
+ const projectsYaml = path.join(configDir, 'projects.yaml');
333
+ const claudeSettings = path.join(os.homedir(), '.claude', 'settings.json');
334
+ let coderConfigured = fs.existsSync(projectsYaml);
335
+ if (!coderConfigured && fs.existsSync(claudeSettings)) {
336
+ try {
337
+ const raw = fs.readFileSync(claudeSettings, 'utf-8');
338
+ coderConfigured = raw.includes('aquaman-coder hook') || raw.includes('aquaman coder hook');
339
+ }
340
+ catch { /* */ }
341
+ }
342
+ console.log('');
343
+ console.log(` ${aqua('Coder integration')}`);
344
+ if (coderConfigured) {
345
+ console.log(` \u2713 configured (deep: ${aqua('aquaman coder doctor')})`);
346
+ }
347
+ else {
348
+ console.log(` \u2022 not configured`);
349
+ console.log(` Your coding agents (Claude Code, Codex, \u2026) could use the same`);
350
+ console.log(` vault protection. Install: ${aqua('npm install -g aquaman-coder')}`);
351
+ }
352
+ console.log('');
353
+ if (issues === 0) {
354
+ console.log(' All baseline checks passed.');
355
+ }
356
+ else {
357
+ console.log(` ${issues} issue${issues > 1 ? 's' : ''} found in baseline. Fix above and re-run.`);
123
358
  }
359
+ console.log('');
360
+ process.exitCode = issues > 0 ? 1 : 0;
124
361
  });
125
- // Start command - launches credential proxy + OpenClaw
362
+ // aquaman status \u2014 proxy overview.
126
363
  program
364
+ .command('status')
365
+ .description('Proxy daemon status overview')
366
+ .action(async () => {
367
+ const config = loadConfig();
368
+ console.log('');
369
+ console.log(` \u{1F531} ${aqua('Aquaman')} ${VERSION} — status`);
370
+ console.log('');
371
+ // Proxy state (probe socket first — the headline number)
372
+ const sockPath = path.join(getConfigDir(), 'proxy.sock');
373
+ let proxyLine = ` ${aqua('Proxy:')} not running`;
374
+ try {
375
+ const body = await new Promise((resolve, reject) => {
376
+ const req = http.request({ socketPath: sockPath, path: '/_health', method: 'GET' }, (res) => {
377
+ let buf = '';
378
+ res.on('data', (c) => { buf += c; });
379
+ res.on('end', () => resolve(buf));
380
+ });
381
+ req.on('error', reject);
382
+ req.end();
383
+ });
384
+ const health = JSON.parse(body);
385
+ proxyLine = ` ${aqua('Proxy:')} running (v${health.version}, uptime ${Math.floor(health.uptime ?? 0)}s)`;
386
+ }
387
+ catch { /* not running */ }
388
+ console.log(proxyLine);
389
+ // Configuration
390
+ console.log('');
391
+ console.log(` ${aqua('Configuration')}`);
392
+ console.log(` Config dir: ${getConfigDir()}`);
393
+ console.log(` Backend: ${config.credentials.backend}`);
394
+ console.log(` Socket: ${sockPath}`);
395
+ console.log(` Audit: ${config.audit.enabled ? 'enabled' : 'disabled'}`);
396
+ // Proxied services
397
+ console.log('');
398
+ console.log(` ${aqua('Proxied services')} (${config.credentials.proxiedServices.length})`);
399
+ for (const svc of config.credentials.proxiedServices)
400
+ console.log(` - ${svc}`);
401
+ console.log('');
402
+ console.log(` For deeper views: ${aqua('aquaman openclaw status')} / ${aqua('aquaman coder status')}`);
403
+ console.log('');
404
+ });
405
+ // ---------------- Shared helper: vault-only setup ----------------
406
+ async function runVaultSetup(opts) {
407
+ const os = await import('node:os');
408
+ const configDir = getConfigDir();
409
+ const configPath = path.join(configDir, 'config.yaml');
410
+ if (!opts.quiet)
411
+ console.log('\n \u{1F531} Vault setup\n');
412
+ // Backend detection
413
+ const platform = os.platform();
414
+ let backend = opts.backend;
415
+ if (!backend) {
416
+ if (platform === 'darwin') {
417
+ backend = 'keychain';
418
+ }
419
+ else {
420
+ try {
421
+ const { execSync } = await import('node:child_process');
422
+ execSync('pkg-config --exists libsecret-1', { stdio: 'pipe' });
423
+ backend = 'keychain';
424
+ }
425
+ catch {
426
+ const { isSystemdCredsAvailable } = await import('../core/credentials/backends/systemd-creds.js');
427
+ backend = isSystemdCredsAvailable() ? 'systemd-creds' : 'encrypted-file';
428
+ }
429
+ }
430
+ }
431
+ const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
432
+ if (!validBackends.includes(backend)) {
433
+ console.error(` Invalid backend: ${backend}. Valid: ${validBackends.join(', ')}`);
434
+ process.exit(1);
435
+ }
436
+ if (!opts.quiet) {
437
+ const platformLabel = platform === 'darwin' ? 'macOS' : platform === 'linux' ? 'Linux' : platform;
438
+ console.log(` Platform: ${platformLabel}`);
439
+ console.log(` Backend: ${backend}\n`);
440
+ }
441
+ ensureConfigDir();
442
+ let config = getDefaultConfig();
443
+ if (fs.existsSync(configPath)) {
444
+ config = loadConfig();
445
+ }
446
+ config.credentials.backend = backend;
447
+ fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
448
+ const auditDir = path.join(configDir, 'audit');
449
+ fs.mkdirSync(auditDir, { recursive: true, mode: 0o700 });
450
+ let store;
451
+ try {
452
+ store = await createCredentialStore({
453
+ backend: config.credentials.backend,
454
+ encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
455
+ vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
456
+ vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
457
+ onePasswordVault: config.credentials.onePasswordVault,
458
+ onePasswordAccount: config.credentials.onePasswordAccount,
459
+ keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
460
+ keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
461
+ bitwardenFolder: config.credentials.bitwardenFolder,
462
+ bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
463
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId
464
+ });
465
+ }
466
+ catch (err) {
467
+ console.error(` Failed to initialize ${backend}: ${err instanceof Error ? err.message : err}`);
468
+ process.exit(1);
469
+ }
470
+ const storedServices = [];
471
+ if (opts.nonInteractive) {
472
+ const anth = process.env['ANTHROPIC_API_KEY'];
473
+ if (anth) {
474
+ await store.set('anthropic', 'api_key', anth);
475
+ storedServices.push('anthropic');
476
+ console.log(' \u2713 Stored anthropic/api_key');
477
+ }
478
+ const op = process.env['OPENAI_API_KEY'];
479
+ if (op) {
480
+ await store.set('openai', 'api_key', op);
481
+ storedServices.push('openai');
482
+ console.log(' \u2713 Stored openai/api_key');
483
+ }
484
+ }
485
+ else {
486
+ const readline = await import('node:readline');
487
+ const promptSecret = (prompt) => new Promise((resolve) => {
488
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
489
+ if (process.stdin.isTTY) {
490
+ process.stdout.write(prompt);
491
+ const stdin = process.stdin;
492
+ stdin.setRawMode(true);
493
+ stdin.resume();
494
+ let input = '';
495
+ const onData = (data) => {
496
+ const char = data.toString();
497
+ if (char === '\n' || char === '\r') {
498
+ stdin.setRawMode(false);
499
+ stdin.removeListener('data', onData);
500
+ stdin.pause();
501
+ rl.close();
502
+ process.stdout.write('\n');
503
+ resolve(input.trim());
504
+ }
505
+ else if (char === '\x7f' || char === '\b') {
506
+ input = input.slice(0, -1);
507
+ }
508
+ else if (char === '\x03') {
509
+ stdin.setRawMode(false);
510
+ process.exit(0);
511
+ }
512
+ else {
513
+ input += char;
514
+ }
515
+ };
516
+ stdin.on('data', onData);
517
+ }
518
+ else {
519
+ rl.question(prompt, (a) => { rl.close(); resolve(a.trim()); });
520
+ }
521
+ });
522
+ const anth = await promptSecret(' ? Anthropic API key (or Enter to skip): ');
523
+ if (anth) {
524
+ await store.set('anthropic', 'api_key', anth);
525
+ storedServices.push('anthropic');
526
+ console.log(' \u2713 anthropic/api_key\n');
527
+ }
528
+ const op = await promptSecret(' ? OpenAI API key (or Enter to skip): ');
529
+ if (op) {
530
+ await store.set('openai', 'api_key', op);
531
+ storedServices.push('openai');
532
+ console.log(' \u2713 openai/api_key\n');
533
+ }
534
+ }
535
+ if (opts.policy && storedServices.length > 0) {
536
+ const presets = getDefaultPolicyPresets();
537
+ const policyToApply = {};
538
+ for (const svc of storedServices) {
539
+ if (presets[svc])
540
+ policyToApply[svc] = presets[svc];
541
+ }
542
+ if (Object.keys(policyToApply).length > 0) {
543
+ config.policy = policyToApply;
544
+ saveConfig(config);
545
+ if (!opts.quiet)
546
+ console.log(' \u2713 Default policy presets applied.\n');
547
+ }
548
+ }
549
+ return { store, storedServices, backend: backend };
550
+ }
551
+ // OpenClaw integration namespace \u2014 full bundle setup, deep doctor/status,
552
+ // migration tools, and the (hidden) plugin-mode entry the plugin spawns.
553
+ const openclaw = program
554
+ .command('openclaw')
555
+ .description('OpenClaw Gateway integration (full setup, deep diagnostics, migration)');
556
+ // openclaw start \u2014 launches credential proxy + OpenClaw
557
+ openclaw
127
558
  .command('start')
128
559
  .description('Start credential proxy and launch OpenClaw')
129
560
  .option('-w, --workspace <path>', 'Workspace directory for OpenClaw')
@@ -375,9 +806,9 @@ program
375
806
  process.on('SIGTERM', shutdown);
376
807
  });
377
808
  // Plugin mode command - for use when managed by OpenClaw plugin
378
- program
379
- .command('plugin-mode')
380
- .description('Run in plugin mode (managed by OpenClaw plugin)')
809
+ openclaw
810
+ .command('plugin-mode', { hidden: true })
811
+ .description('Run in plugin mode (invoked by OpenClaw plugin, not by humans)')
381
812
  .action(async () => {
382
813
  const config = loadConfig();
383
814
  const socketPath = path.join(getConfigDir(), 'proxy.sock');
@@ -458,8 +889,8 @@ program
458
889
  process.on('SIGINT', shutdown);
459
890
  process.on('SIGTERM', shutdown);
460
891
  });
461
- // Configure command - write OpenClaw environment configuration
462
- program
892
+ // openclaw configure write OpenClaw environment configuration
893
+ openclaw
463
894
  .command('configure')
464
895
  .description('Generate environment configuration for OpenClaw')
465
896
  .option('--method <method>', 'Output method: env, dotenv, shell-rc', 'env')
@@ -524,11 +955,13 @@ program
524
955
  }
525
956
  });
526
957
  // Setup command - all-in-one guided onboarding
527
- program
958
+ // openclaw setup — full OpenClaw bundle: vault wizard + plugin install +
959
+ // openclaw.json wiring + auth-profiles.json + optional auto-migration.
960
+ openclaw
528
961
  .command('setup')
529
- .description('All-in-one setup wizardcreates config, stores credentials, installs plugin')
962
+ .description('Full setup for OpenClaw vault + plugin + auth profiles + (optional) auto-migrate')
530
963
  .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
531
- .option('--no-openclaw', 'Skip OpenClaw plugin installation')
964
+ .option('--no-openclaw', 'Skip OpenClaw plugin installation step (run vault setup only)')
532
965
  .option('--no-policy', 'Skip request policy preset configuration')
533
966
  .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
534
967
  .action(async (options) => {
@@ -930,6 +1363,26 @@ program
930
1363
  fs.mkdirSync(profilesDir, { recursive: true, mode: 0o700 });
931
1364
  fs.writeFileSync(profilesPath, JSON.stringify({ version: 1, profiles, order }, null, 2), { mode: 0o600 });
932
1365
  console.log(' \u2713 Auth profiles generated at ' + profilesPath);
1366
+ // OpenClaw >= 2026.6.5 reads provider auth profiles from each agent's
1367
+ // openclaw-agent.sqlite, not auth-profiles.json (openclaw/openclaw#89102).
1368
+ // The placeholder above must be imported into SQLite once with
1369
+ // `openclaw doctor --fix`. We deliberately do NOT auto-run that here:
1370
+ // it is OpenClaw's broad config-healing command \u2014 verified to take
1371
+ // ~19s (gateway probes) and to rewrite openclaw.json (it reset our
1372
+ // plugin entry to bundled defaults in testing). `aquaman openclaw
1373
+ // doctor` detects the SQLite-only case and prints the import step so
1374
+ // the operator runs it intentionally. Holistic fix (SecretRef
1375
+ // provider manifest) is tracked for the next plugin touch.
1376
+ let ocVersion = null;
1377
+ try {
1378
+ const { execSync } = await import('node:child_process');
1379
+ ocVersion = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
1380
+ }
1381
+ catch { /* CLI not on PATH */ }
1382
+ if (authProfilesAreSqliteOnly(ocVersion)) {
1383
+ console.log(' \u2192 OpenClaw \u2265 2026.6.5 reads auth profiles from SQLite; import the placeholder once:');
1384
+ console.log(' openclaw doctor --fix (backs up openclaw.json first; see aquaman openclaw doctor)');
1385
+ }
933
1386
  }
934
1387
  }
935
1388
  }
@@ -983,10 +1436,12 @@ program
983
1436
  console.log(' Troubleshooting: aquaman doctor');
984
1437
  console.log('');
985
1438
  });
986
- // Doctor command - diagnostic tool
987
- program
1439
+ // openclaw doctor deep diagnostic for the OpenClaw integration. Includes
1440
+ // the agent-agnostic vault/audit/policy checks too so a single command gives
1441
+ // the OpenClaw operator the full picture.
1442
+ openclaw
988
1443
  .command('doctor')
989
- .description('Check aquaman configuration and diagnose issues')
1444
+ .description('Deep diagnostic for the OpenClaw integration (vault + plugin + auth profiles)')
990
1445
  .action(async () => {
991
1446
  const os = await import('node:os');
992
1447
  const configDir = getConfigDir();
@@ -994,7 +1449,7 @@ program
994
1449
  const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
995
1450
  let issues = 0;
996
1451
  console.log('');
997
- console.log(` \u{1F531}\u{1F99E} Aquaman ${VERSION} \u2014 Welcome to the doctor\u2019s office.`);
1452
+ console.log(` \u{1F531} Aquaman ${VERSION} \u2014 Welcome to the doctor\u2019s office.`);
998
1453
  console.log('');
999
1454
  // 1. Config file
1000
1455
  if (fs.existsSync(configPath)) {
@@ -1167,6 +1622,7 @@ program
1167
1622
  }
1168
1623
  // 6. OpenClaw detection
1169
1624
  let openclawDetected = false;
1625
+ let openclawVersion = null;
1170
1626
  let cliFound = false;
1171
1627
  try {
1172
1628
  const { execSync } = await import('node:child_process');
@@ -1178,6 +1634,7 @@ program
1178
1634
  try {
1179
1635
  const { execSync } = await import('node:child_process');
1180
1636
  const ver = execSync('openclaw --version', { stdio: 'pipe', encoding: 'utf-8', timeout: 5000 }).trim();
1637
+ openclawVersion = ver;
1181
1638
  console.log(` \u2713 ${aqua('OpenClaw')} installed (${ver})`);
1182
1639
  }
1183
1640
  catch {
@@ -1260,9 +1717,31 @@ program
1260
1717
  // openclaw.json invalid — already reported above
1261
1718
  }
1262
1719
  }
1263
- // 9. Auth profiles exist
1264
- const profilesPath = path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json');
1265
- if (fs.existsSync(profilesPath)) {
1720
+ // 9. Auth profiles. OpenClaw >= 2026.6.5 reads provider auth profiles from
1721
+ // each agent's openclaw-agent.sqlite and no longer reads
1722
+ // auth-profiles.json at runtime (openclaw/openclaw#89102). On those
1723
+ // versions the placeholder the plugin writes to the JSON file must be
1724
+ // imported into SQLite once via `openclaw doctor --fix`.
1725
+ const agentDir = path.join(openclawStateDir, 'agents', 'main', 'agent');
1726
+ const profilesPath = path.join(agentDir, 'auth-profiles.json');
1727
+ const sqlitePath = path.join(agentDir, 'openclaw-agent.sqlite');
1728
+ if (authProfilesAreSqliteOnly(openclawVersion)) {
1729
+ if (fs.existsSync(profilesPath)) {
1730
+ console.log(` \u2717 ${aqua('Auth profiles')} JSON present but ${openclawVersion} reads from SQLite`);
1731
+ console.log(' \u2192 Import the placeholder once: openclaw doctor --fix (backs up openclaw.json first)');
1732
+ console.log(' (OpenClaw \u2265 2026.6.5 dropped the runtime auth-profiles.json read path \u2014 openclaw/openclaw#89102)');
1733
+ issues++;
1734
+ }
1735
+ else if (fs.existsSync(sqlitePath)) {
1736
+ console.log(` \u2713 ${aqua('Auth profiles')} (SQLite store present; OpenClaw \u2265 2026.6.5)`);
1737
+ }
1738
+ else {
1739
+ console.log(` \u2717 ${aqua('Auth profiles')} missing (no SQLite store found)`);
1740
+ console.log(' \u2192 Run: aquaman setup, then: openclaw doctor --fix');
1741
+ issues++;
1742
+ }
1743
+ }
1744
+ else if (fs.existsSync(profilesPath)) {
1266
1745
  console.log(` \u2713 ${aqua('Auth profiles')} exist`);
1267
1746
  }
1268
1747
  else {
@@ -1323,7 +1802,7 @@ program
1323
1802
  for (const c of unmigrated) {
1324
1803
  console.log(` ${c.service}/${c.key} \u2190 ${c.source}`);
1325
1804
  }
1326
- console.log(' \u2192 Run: aquaman migrate openclaw --auto');
1805
+ console.log(' \u2192 Run: aquaman openclaw migrate --auto');
1327
1806
  issues++;
1328
1807
  }
1329
1808
  if (needsCleanup.length > 0) {
@@ -1454,38 +1933,64 @@ credentials
1454
1933
  console.error('Credential store not available:', error instanceof Error ? error.message : error);
1455
1934
  process.exit(1);
1456
1935
  }
1457
- // Read value from stdin
1458
- console.log(`Enter value for ${service}/${key} (input hidden):`);
1459
- const readline = await import('node:readline');
1460
- const rl = readline.createInterface({
1461
- input: process.stdin,
1462
- output: process.stdout
1463
- });
1464
- // Disable echo
1465
- if (process.stdin.isTTY) {
1466
- process.stdin.setRawMode(true);
1467
- }
1468
- let value = '';
1469
- process.stdin.on('data', (data) => {
1470
- const char = data.toString();
1471
- if (char === '\n' || char === '\r') {
1472
- process.stdin.setRawMode(false);
1473
- rl.close();
1474
- storeCredential();
1475
- }
1476
- else if (char === '\x7f' || char === '\b') {
1477
- value = value.slice(0, -1);
1478
- }
1479
- else {
1480
- value += char;
1481
- }
1482
- });
1483
- async function storeCredential() {
1484
- console.log('');
1485
- await store.set(service, key, value.trim());
1486
- console.log(`Credential stored: ${service}/${key}`);
1936
+ // Two stdin paths:
1937
+ // - TTY (interactive shell): per-character raw-mode read so the value
1938
+ // never echoes. Submit on Enter, backspace on DEL/^H.
1939
+ // - Pipe (scripts / CI / migrations): read all stdin into one buffer.
1940
+ // Strip exactly ONE trailing newline so `printf x | ...` and
1941
+ // `echo x | ...` both round-trip to `x` without mangling embedded
1942
+ // newlines in PEM keys or JSON blobs.
1943
+ const value = process.stdin.isTTY
1944
+ ? await readTtyHiddenInput(`Enter value for ${service}/${key} (input hidden):`)
1945
+ : await readAllStdin();
1946
+ if (value.length === 0) {
1947
+ console.error('Empty credential value rejected. Pipe a value or type one.');
1948
+ process.exit(1);
1487
1949
  }
1950
+ await store.set(service, key, value);
1951
+ console.log(`Credential stored: ${service}/${key}`);
1488
1952
  });
1953
+ async function readAllStdin() {
1954
+ const chunks = [];
1955
+ for await (const chunk of process.stdin) {
1956
+ chunks.push(chunk);
1957
+ }
1958
+ const raw = Buffer.concat(chunks).toString('utf-8');
1959
+ // Strip a single trailing \n (and the optional \r before it for CRLF).
1960
+ return raw.endsWith('\n') ? raw.replace(/\r?\n$/, '') : raw;
1961
+ }
1962
+ async function readTtyHiddenInput(prompt) {
1963
+ console.log(prompt);
1964
+ process.stdin.setRawMode(true);
1965
+ process.stdin.resume();
1966
+ process.stdin.setEncoding('utf-8');
1967
+ return new Promise((resolve) => {
1968
+ let value = '';
1969
+ const onData = (chunk) => {
1970
+ for (const char of chunk) {
1971
+ if (char === '\n' || char === '\r') {
1972
+ process.stdin.setRawMode(false);
1973
+ process.stdin.pause();
1974
+ process.stdin.off('data', onData);
1975
+ console.log('');
1976
+ resolve(value);
1977
+ return;
1978
+ }
1979
+ if (char === '\x7f' || char === '\b') {
1980
+ value = value.slice(0, -1);
1981
+ }
1982
+ else if (char === '\x03') { // Ctrl-C
1983
+ process.stdin.setRawMode(false);
1984
+ process.exit(130);
1985
+ }
1986
+ else {
1987
+ value += char;
1988
+ }
1989
+ }
1990
+ };
1991
+ process.stdin.on('data', onData);
1992
+ });
1993
+ }
1489
1994
  credentials
1490
1995
  .command('list')
1491
1996
  .description('List stored credentials')
@@ -1600,7 +2105,10 @@ credentials
1600
2105
  console.log(` vault kv put secret/aquaman/${name}/${key} credential="YOUR_KEY"`);
1601
2106
  break;
1602
2107
  case '1password':
1603
- console.log(` op item create --vault aquaman --category "API Credential" --title aquaman-${name}-${key} credential="YOUR_KEY"`);
2108
+ // Prefer aquaman's own CLI it pipes the value via a 0o600 temp
2109
+ // template file, never exposing the secret on argv or in op's
2110
+ // /proc/<pid>/cmdline.
2111
+ console.log(` aquaman credentials add ${name} ${key}`);
1604
2112
  break;
1605
2113
  default:
1606
2114
  console.log(` aquaman credentials add ${name} ${key}`);
@@ -1718,9 +2226,8 @@ policy
1718
2226
  }
1719
2227
  });
1720
2228
  // Migration commands
1721
- const migrate = program.command('migrate').description('Migrate credentials from other sources');
1722
- migrate
1723
- .command('openclaw')
2229
+ openclaw
2230
+ .command('migrate')
1724
2231
  .description('Migrate channel credentials from openclaw.json into aquaman')
1725
2232
  .option('-c, --config <path>', 'Path to openclaw.json')
1726
2233
  .option('--dry-run', 'Show what would be migrated without writing')
@@ -1741,7 +2248,7 @@ migrate
1741
2248
  console.error('No aquaman config found. Run `aquaman setup` first.');
1742
2249
  process.exit(1);
1743
2250
  }
1744
- console.log(`\n \u{1F531}\u{1F99E} Aquaman ${aqua(VERSION)} \u2014 Time to put your secrets somewhere safe.\n`);
2251
+ console.log(`\n \u{1F531} Aquaman ${aqua(VERSION)} \u2014 Time to put your secrets somewhere safe.\n`);
1745
2252
  console.log(' Scanning for plaintext credentials...\n');
1746
2253
  const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
1747
2254
  const openclawConfigPath = findOpenClawConfig(opts.config || path.join(openclawStateDir, 'openclaw.json'));
@@ -2109,9 +2616,10 @@ migrate
2109
2616
  }
2110
2617
  });
2111
2618
  // Status command
2112
- program
2619
+ // openclaw status — deep status for the OpenClaw integration.
2620
+ openclaw
2113
2621
  .command('status')
2114
- .description('Show aquaman status')
2622
+ .description('OpenClaw-specific status (plugin lifecycle, sentinel env vars)')
2115
2623
  .action(async () => {
2116
2624
  const config = loadConfig();
2117
2625
  console.log('aquaman status\n');
@@ -2187,6 +2695,47 @@ function formatEntry(entry) {
2187
2695
  return JSON.stringify(entry.data).slice(0, 80);
2188
2696
  }
2189
2697
  }
2698
+ // ---------------- coder namespace (shim → aquaman-coder bin) ----------------
2699
+ //
2700
+ // The `aquaman coder *` namespace presents a unified user-facing surface for
2701
+ // the coding-agent adapter. The actual implementation lives in the separate
2702
+ // `aquaman-coder` package (see packages/coder/). This shim execs that binary
2703
+ // with the remaining argv, so the proxy CLI never imports coder code — the
2704
+ // `proxy → coder ✗` boundary in docs/PACKAGES.md stays intact.
2705
+ //
2706
+ // If aquaman-coder isn't installed, we print a clear install hint.
2707
+ // Intercept `aquaman coder ...` BEFORE Commander parses it, so the catch-all
2708
+ // behavior is exact: every token after `coder` flows through to the
2709
+ // aquaman-coder binary verbatim, including --flags Commander would otherwise
2710
+ // interpret as unknown options.
2711
+ //
2712
+ // Only intercept when `coder` is the first command token (argv[2]). This
2713
+ // avoids false positives like `aquaman policy test coder/api /foo` where
2714
+ // `coder` appears as a positional argument elsewhere.
2715
+ if (process.argv[2] === 'coder') {
2716
+ const subArgs = process.argv.slice(3);
2717
+ const { spawnSync } = await import('node:child_process');
2718
+ const result = spawnSync('aquaman-coder', subArgs, { stdio: 'inherit' });
2719
+ if (result.error && result.error.code === 'ENOENT') {
2720
+ console.error('aquaman-coder is not installed.');
2721
+ console.error('Install it with: npm install -g aquaman-coder');
2722
+ process.exit(127);
2723
+ }
2724
+ if (result.error) {
2725
+ console.error(`Failed to run aquaman-coder: ${result.error.message}`);
2726
+ process.exit(1);
2727
+ }
2728
+ process.exit(result.status ?? 0);
2729
+ }
2730
+ // Register the `coder` namespace so it shows up in `aquaman --help`.
2731
+ // Its action is unreachable (the intercept above bypasses Commander entirely),
2732
+ // but documentation matters.
2733
+ program
2734
+ .command('coder')
2735
+ .description('AI coding-agent integration (delegates to `aquaman-coder`)')
2736
+ .allowUnknownOption()
2737
+ .helpOption(false)
2738
+ .action(() => { });
2190
2739
  // Show help when run without arguments (like openclaw does)
2191
2740
  if (process.argv.length <= 2) {
2192
2741
  program.help();