aquaman-proxy 0.11.4 → 0.12.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 +49 -56
- package/dist/cli/index.js +560 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/core/audit/logger.d.ts +21 -0
- package/dist/core/audit/logger.d.ts.map +1 -1
- package/dist/core/audit/logger.js +46 -0
- package/dist/core/audit/logger.js.map +1 -1
- package/dist/core/credentials/backends/onepassword.d.ts +2 -0
- package/dist/core/credentials/backends/onepassword.d.ts.map +1 -1
- package/dist/core/credentials/backends/onepassword.js +75 -33
- package/dist/core/credentials/backends/onepassword.js.map +1 -1
- package/dist/core/credentials/index.d.ts +1 -1
- package/dist/core/credentials/index.d.ts.map +1 -1
- package/dist/core/credentials/index.js +1 -1
- package/dist/core/credentials/index.js.map +1 -1
- package/dist/core/credentials/store.d.ts.map +1 -1
- package/dist/core/credentials/store.js +2 -4
- package/dist/core/credentials/store.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/redactor/index.d.ts +96 -0
- package/dist/core/redactor/index.d.ts.map +1 -0
- package/dist/core/redactor/index.js +230 -0
- package/dist/core/redactor/index.js.map +1 -0
- package/dist/daemon.d.ts +20 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +147 -0
- package/dist/daemon.js.map +1 -1
- package/package.json +5 -5
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';
|
|
@@ -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
|
|
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
|
-
|
|
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');
|
|
123
211
|
}
|
|
124
212
|
});
|
|
125
|
-
//
|
|
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.
|
|
126
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.`);
|
|
358
|
+
}
|
|
359
|
+
console.log('');
|
|
360
|
+
process.exitCode = issues > 0 ? 1 : 0;
|
|
361
|
+
});
|
|
362
|
+
// aquaman status \u2014 proxy overview.
|
|
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
|
-
|
|
379
|
-
.command('plugin-mode')
|
|
380
|
-
.description('Run in plugin mode (
|
|
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
|
-
//
|
|
462
|
-
|
|
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
|
-
|
|
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('
|
|
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) => {
|
|
@@ -983,10 +1416,12 @@ program
|
|
|
983
1416
|
console.log(' Troubleshooting: aquaman doctor');
|
|
984
1417
|
console.log('');
|
|
985
1418
|
});
|
|
986
|
-
//
|
|
987
|
-
|
|
1419
|
+
// openclaw doctor — deep diagnostic for the OpenClaw integration. Includes
|
|
1420
|
+
// the agent-agnostic vault/audit/policy checks too so a single command gives
|
|
1421
|
+
// the OpenClaw operator the full picture.
|
|
1422
|
+
openclaw
|
|
988
1423
|
.command('doctor')
|
|
989
|
-
.description('
|
|
1424
|
+
.description('Deep diagnostic for the OpenClaw integration (vault + plugin + auth profiles)')
|
|
990
1425
|
.action(async () => {
|
|
991
1426
|
const os = await import('node:os');
|
|
992
1427
|
const configDir = getConfigDir();
|
|
@@ -994,7 +1429,7 @@ program
|
|
|
994
1429
|
const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
|
|
995
1430
|
let issues = 0;
|
|
996
1431
|
console.log('');
|
|
997
|
-
console.log(` \u{1F531}
|
|
1432
|
+
console.log(` \u{1F531} Aquaman ${VERSION} \u2014 Welcome to the doctor\u2019s office.`);
|
|
998
1433
|
console.log('');
|
|
999
1434
|
// 1. Config file
|
|
1000
1435
|
if (fs.existsSync(configPath)) {
|
|
@@ -1323,7 +1758,7 @@ program
|
|
|
1323
1758
|
for (const c of unmigrated) {
|
|
1324
1759
|
console.log(` ${c.service}/${c.key} \u2190 ${c.source}`);
|
|
1325
1760
|
}
|
|
1326
|
-
console.log(' \u2192 Run: aquaman migrate
|
|
1761
|
+
console.log(' \u2192 Run: aquaman openclaw migrate --auto');
|
|
1327
1762
|
issues++;
|
|
1328
1763
|
}
|
|
1329
1764
|
if (needsCleanup.length > 0) {
|
|
@@ -1454,38 +1889,64 @@ credentials
|
|
|
1454
1889
|
console.error('Credential store not available:', error instanceof Error ? error.message : error);
|
|
1455
1890
|
process.exit(1);
|
|
1456
1891
|
}
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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}`);
|
|
1892
|
+
// Two stdin paths:
|
|
1893
|
+
// - TTY (interactive shell): per-character raw-mode read so the value
|
|
1894
|
+
// never echoes. Submit on Enter, backspace on DEL/^H.
|
|
1895
|
+
// - Pipe (scripts / CI / migrations): read all stdin into one buffer.
|
|
1896
|
+
// Strip exactly ONE trailing newline so `printf x | ...` and
|
|
1897
|
+
// `echo x | ...` both round-trip to `x` without mangling embedded
|
|
1898
|
+
// newlines in PEM keys or JSON blobs.
|
|
1899
|
+
const value = process.stdin.isTTY
|
|
1900
|
+
? await readTtyHiddenInput(`Enter value for ${service}/${key} (input hidden):`)
|
|
1901
|
+
: await readAllStdin();
|
|
1902
|
+
if (value.length === 0) {
|
|
1903
|
+
console.error('Empty credential value rejected. Pipe a value or type one.');
|
|
1904
|
+
process.exit(1);
|
|
1487
1905
|
}
|
|
1906
|
+
await store.set(service, key, value);
|
|
1907
|
+
console.log(`Credential stored: ${service}/${key}`);
|
|
1488
1908
|
});
|
|
1909
|
+
async function readAllStdin() {
|
|
1910
|
+
const chunks = [];
|
|
1911
|
+
for await (const chunk of process.stdin) {
|
|
1912
|
+
chunks.push(chunk);
|
|
1913
|
+
}
|
|
1914
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
1915
|
+
// Strip a single trailing \n (and the optional \r before it for CRLF).
|
|
1916
|
+
return raw.endsWith('\n') ? raw.replace(/\r?\n$/, '') : raw;
|
|
1917
|
+
}
|
|
1918
|
+
async function readTtyHiddenInput(prompt) {
|
|
1919
|
+
console.log(prompt);
|
|
1920
|
+
process.stdin.setRawMode(true);
|
|
1921
|
+
process.stdin.resume();
|
|
1922
|
+
process.stdin.setEncoding('utf-8');
|
|
1923
|
+
return new Promise((resolve) => {
|
|
1924
|
+
let value = '';
|
|
1925
|
+
const onData = (chunk) => {
|
|
1926
|
+
for (const char of chunk) {
|
|
1927
|
+
if (char === '\n' || char === '\r') {
|
|
1928
|
+
process.stdin.setRawMode(false);
|
|
1929
|
+
process.stdin.pause();
|
|
1930
|
+
process.stdin.off('data', onData);
|
|
1931
|
+
console.log('');
|
|
1932
|
+
resolve(value);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (char === '\x7f' || char === '\b') {
|
|
1936
|
+
value = value.slice(0, -1);
|
|
1937
|
+
}
|
|
1938
|
+
else if (char === '\x03') { // Ctrl-C
|
|
1939
|
+
process.stdin.setRawMode(false);
|
|
1940
|
+
process.exit(130);
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
value += char;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
process.stdin.on('data', onData);
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1489
1950
|
credentials
|
|
1490
1951
|
.command('list')
|
|
1491
1952
|
.description('List stored credentials')
|
|
@@ -1600,7 +2061,10 @@ credentials
|
|
|
1600
2061
|
console.log(` vault kv put secret/aquaman/${name}/${key} credential="YOUR_KEY"`);
|
|
1601
2062
|
break;
|
|
1602
2063
|
case '1password':
|
|
1603
|
-
|
|
2064
|
+
// Prefer aquaman's own CLI — it pipes the value via a 0o600 temp
|
|
2065
|
+
// template file, never exposing the secret on argv or in op's
|
|
2066
|
+
// /proc/<pid>/cmdline.
|
|
2067
|
+
console.log(` aquaman credentials add ${name} ${key}`);
|
|
1604
2068
|
break;
|
|
1605
2069
|
default:
|
|
1606
2070
|
console.log(` aquaman credentials add ${name} ${key}`);
|
|
@@ -1718,9 +2182,8 @@ policy
|
|
|
1718
2182
|
}
|
|
1719
2183
|
});
|
|
1720
2184
|
// Migration commands
|
|
1721
|
-
|
|
1722
|
-
migrate
|
|
1723
|
-
.command('openclaw')
|
|
2185
|
+
openclaw
|
|
2186
|
+
.command('migrate')
|
|
1724
2187
|
.description('Migrate channel credentials from openclaw.json into aquaman')
|
|
1725
2188
|
.option('-c, --config <path>', 'Path to openclaw.json')
|
|
1726
2189
|
.option('--dry-run', 'Show what would be migrated without writing')
|
|
@@ -1741,7 +2204,7 @@ migrate
|
|
|
1741
2204
|
console.error('No aquaman config found. Run `aquaman setup` first.');
|
|
1742
2205
|
process.exit(1);
|
|
1743
2206
|
}
|
|
1744
|
-
console.log(`\n \u{1F531}
|
|
2207
|
+
console.log(`\n \u{1F531} Aquaman ${aqua(VERSION)} \u2014 Time to put your secrets somewhere safe.\n`);
|
|
1745
2208
|
console.log(' Scanning for plaintext credentials...\n');
|
|
1746
2209
|
const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
|
|
1747
2210
|
const openclawConfigPath = findOpenClawConfig(opts.config || path.join(openclawStateDir, 'openclaw.json'));
|
|
@@ -2109,9 +2572,10 @@ migrate
|
|
|
2109
2572
|
}
|
|
2110
2573
|
});
|
|
2111
2574
|
// Status command
|
|
2112
|
-
|
|
2575
|
+
// openclaw status — deep status for the OpenClaw integration.
|
|
2576
|
+
openclaw
|
|
2113
2577
|
.command('status')
|
|
2114
|
-
.description('
|
|
2578
|
+
.description('OpenClaw-specific status (plugin lifecycle, sentinel env vars)')
|
|
2115
2579
|
.action(async () => {
|
|
2116
2580
|
const config = loadConfig();
|
|
2117
2581
|
console.log('aquaman status\n');
|
|
@@ -2187,6 +2651,47 @@ function formatEntry(entry) {
|
|
|
2187
2651
|
return JSON.stringify(entry.data).slice(0, 80);
|
|
2188
2652
|
}
|
|
2189
2653
|
}
|
|
2654
|
+
// ---------------- coder namespace (shim → aquaman-coder bin) ----------------
|
|
2655
|
+
//
|
|
2656
|
+
// The `aquaman coder *` namespace presents a unified user-facing surface for
|
|
2657
|
+
// the coding-agent adapter. The actual implementation lives in the separate
|
|
2658
|
+
// `aquaman-coder` package (see packages/coder/). This shim execs that binary
|
|
2659
|
+
// with the remaining argv, so the proxy CLI never imports coder code — the
|
|
2660
|
+
// `proxy → coder ✗` boundary in docs/PACKAGES.md stays intact.
|
|
2661
|
+
//
|
|
2662
|
+
// If aquaman-coder isn't installed, we print a clear install hint.
|
|
2663
|
+
// Intercept `aquaman coder ...` BEFORE Commander parses it, so the catch-all
|
|
2664
|
+
// behavior is exact: every token after `coder` flows through to the
|
|
2665
|
+
// aquaman-coder binary verbatim, including --flags Commander would otherwise
|
|
2666
|
+
// interpret as unknown options.
|
|
2667
|
+
//
|
|
2668
|
+
// Only intercept when `coder` is the first command token (argv[2]). This
|
|
2669
|
+
// avoids false positives like `aquaman policy test coder/api /foo` where
|
|
2670
|
+
// `coder` appears as a positional argument elsewhere.
|
|
2671
|
+
if (process.argv[2] === 'coder') {
|
|
2672
|
+
const subArgs = process.argv.slice(3);
|
|
2673
|
+
const { spawnSync } = await import('node:child_process');
|
|
2674
|
+
const result = spawnSync('aquaman-coder', subArgs, { stdio: 'inherit' });
|
|
2675
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
2676
|
+
console.error('aquaman-coder is not installed.');
|
|
2677
|
+
console.error('Install it with: npm install -g aquaman-coder');
|
|
2678
|
+
process.exit(127);
|
|
2679
|
+
}
|
|
2680
|
+
if (result.error) {
|
|
2681
|
+
console.error(`Failed to run aquaman-coder: ${result.error.message}`);
|
|
2682
|
+
process.exit(1);
|
|
2683
|
+
}
|
|
2684
|
+
process.exit(result.status ?? 0);
|
|
2685
|
+
}
|
|
2686
|
+
// Register the `coder` namespace so it shows up in `aquaman --help`.
|
|
2687
|
+
// Its action is unreachable (the intercept above bypasses Commander entirely),
|
|
2688
|
+
// but documentation matters.
|
|
2689
|
+
program
|
|
2690
|
+
.command('coder')
|
|
2691
|
+
.description('AI coding-agent integration (delegates to `aquaman-coder`)')
|
|
2692
|
+
.allowUnknownOption()
|
|
2693
|
+
.helpOption(false)
|
|
2694
|
+
.action(() => { });
|
|
2190
2695
|
// Show help when run without arguments (like openclaw does)
|
|
2191
2696
|
if (process.argv.length <= 2) {
|
|
2192
2697
|
program.help();
|