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