aquaman-proxy 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,18 @@
1
1
  # aquaman-proxy
2
2
 
3
- Credential isolation proxy and CLI for [aquaman](https://github.com/tech4242/aquaman).
4
-
5
- ## How It Works
3
+ The proxy daemon and CLI for [aquaman](https://github.com/tech4242/aquaman) — credential isolation for OpenClaw.
6
4
 
7
5
  ```
8
6
  Agent / OpenClaw Gateway Aquaman Proxy
9
7
  ┌──────────────────────┐ ┌──────────────────────┐
10
8
  │ │ │ │
11
- │ ANTHROPIC_BASE_URL │══ Unix ════>│ Keychain / 1Pass / │
12
- │ = aquaman.local │ Domain │ Vault / Encrypted │
13
- │ │<═ Socket ═══│
14
- │ fetch() interceptor │══ (UDS) ══=>│ + Auth injected:
15
- │ redirects channel │ │ header / url-path
16
- │ API traffic │ │ basic / oauth
9
+ │ ANTHROPIC_BASE_URL │══ Unix ═════>│ Keychain / 1Pass / │
10
+ │ = aquaman.local │ Domain │ Vault / Encrypted │
11
+ │ │<═ Socket ════│
12
+ │ fetch() interceptor │══ (UDS) ════>│ + Policy enforced
13
+ │ redirects channel │ │ + Auth injected:
14
+ │ API traffic │ │ header / url-path
15
+ │ │ │ basic / oauth │
17
16
  │ │ │ │
18
17
  │ No credentials. │ ~/.aquaman/ │ │
19
18
  │ No open ports. │ proxy.sock │ │
@@ -30,33 +29,22 @@ Agent / OpenClaw Gateway Aquaman Proxy
30
29
  slack.com/api ...
31
30
  ```
32
31
 
33
- This package is the right side. A reverse proxy that listens on a Unix domain socket (`~/.aquaman/proxy.sock`) and injects credentials from secure backends. No TCP port, no network exposure. 25 builtin services, six auth modes.
32
+ This package is the right side a reverse proxy on a Unix domain socket that stores credentials in secure backends, enforces request policies, injects auth headers, and logs every access. The agent never sees a key.
34
33
 
35
34
  ## Quick Start
36
35
 
37
- With OpenClaw:
38
-
39
36
  ```bash
40
- npm install -g aquaman-proxy # install the proxy CLI
41
- aquaman setup # stores keys, installs plugin, configures OpenClaw
42
- openclaw # proxy starts automatically via plugin
37
+ npm install -g aquaman-proxy
38
+ aquaman setup # stores keys, installs OpenClaw plugin, applies policy defaults
39
+ openclaw # proxy starts automatically via plugin
43
40
  ```
44
41
 
45
- > `aquaman setup` auto-detects your credential backend. macOS defaults to Keychain,
46
- > Linux defaults to encrypted file. Override with `--backend`:
47
- > `aquaman setup --backend keepassxc`
48
- > Options: `keychain`, `encrypted-file`, `keepassxc`, `1password`, `vault`, `systemd-creds`, `bitwarden`
49
-
50
- Existing plaintext credentials are migrated automatically during setup.
51
- Run again anytime to migrate new credentials: `aquaman migrate openclaw --auto`
52
-
53
- Standalone:
42
+ Standalone (without OpenClaw):
54
43
 
55
44
  ```bash
56
- npm install -g aquaman-proxy
57
45
  aquaman init
58
46
  aquaman credentials add anthropic api_key
59
- aquaman daemon # listens on ~/.aquaman/proxy.sock
47
+ aquaman daemon # listens on ~/.aquaman/proxy.sock
60
48
  ```
61
49
 
62
50
  Troubleshooting: `aquaman doctor`
@@ -65,7 +53,7 @@ Troubleshooting: `aquaman doctor`
65
53
 
66
54
  | Command | Description |
67
55
  |---------|-------------|
68
- | `aquaman setup` | Guided onboarding (stores keys, installs plugin) |
56
+ | `aquaman setup` | Guided onboarding (stores keys, installs plugin, applies policy defaults) |
69
57
  | `aquaman doctor` | Diagnose issues with actionable fixes |
70
58
  | `aquaman credentials add <svc> <key>` | Store a credential |
71
59
  | `aquaman credentials list` | List stored credentials |
@@ -74,6 +62,8 @@ Troubleshooting: `aquaman doctor`
74
62
  | `aquaman start` | Start proxy + launch OpenClaw |
75
63
  | `aquaman stop` | Stop running proxy |
76
64
  | `aquaman status` | Show config and proxy status |
65
+ | `aquaman policy list` | List configured policy rules |
66
+ | `aquaman policy test <svc> <method> <path>` | Dry-run a request against policy rules |
77
67
  | `aquaman audit tail` | Recent audit entries |
78
68
  | `aquaman audit verify` | Verify hash chain integrity |
79
69
 
@@ -88,9 +78,20 @@ Troubleshooting: `aquaman doctor`
88
78
  | **Channels (OAuth)** | MS Teams, Feishu, Google Chat |
89
79
  | **At-rest only** | Nostr, Tlon |
90
80
 
81
+ ## Security
82
+
83
+ The proxy enforces four layers of protection:
84
+
85
+ - **Process isolation** — credentials in a separate address space, connected via UDS (`chmod 600`)
86
+ - **Service allowlisting** — `proxiedServices` controls which APIs the agent can reach
87
+ - **Request policies** — method + path rules per service, checked *before* credential injection ([details](https://github.com/tech4242/aquaman#request-policies))
88
+ - **Audit trail** — SHA-256 hash-chained logs of every credential use
89
+
90
+ 7 credential backends: Keychain, 1Password, Vault, Bitwarden, KeePassXC, systemd-creds, encrypted-file.
91
+
91
92
  ## Documentation
92
93
 
93
- See the [main README](https://github.com/tech4242/aquaman#readme) for architecture, credential backends, Docker deployment, and configuration.
94
+ See the [main README](https://github.com/tech4242/aquaman#readme) for the full security model, request policy config, Docker deployment, and architecture diagrams.
94
95
 
95
96
  ## License
96
97
 
package/dist/cli/index.js CHANGED
@@ -17,6 +17,7 @@ import { fileURLToPath } from 'node:url';
17
17
  import { createCredentialProxy } from '../daemon.js';
18
18
  import { createServiceRegistry, ServiceRegistry } from '../service-registry.js';
19
19
  import { createOpenClawIntegration } from '../openclaw/integration.js';
20
+ import { loadPolicyFromConfig, validatePolicyConfig, getDefaultPolicyPresets, matchPolicy } from '../request-policy.js';
20
21
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
21
22
  // Read version from package.json (single source of truth)
22
23
  const __cliFilename = fileURLToPath(import.meta.url);
@@ -28,6 +29,14 @@ const VERSION = pkgJson.version;
28
29
  const noColor = process.env['NO_COLOR'] !== undefined ||
29
30
  (!process.stdout.isTTY && process.env['FORCE_COLOR'] === undefined);
30
31
  const aqua = (s) => noColor ? s : `\x1b[38;2;127;255;212m${s}\x1b[0m`;
32
+ // Credential name validation — shared pattern from daemon.ts and systemd-creds backend
33
+ const SAFE_CRED_NAME = /^[a-z0-9][a-z0-9._-]*$/;
34
+ function validateCredName(label, value) {
35
+ if (!SAFE_CRED_NAME.test(value)) {
36
+ console.error(`Invalid ${label}: "${value}". Allowed: lowercase alphanumeric, dots, hyphens, underscores.`);
37
+ process.exit(1);
38
+ }
39
+ }
31
40
  // PID file management
32
41
  const getPidFile = () => path.join(getConfigDir(), 'daemon.pid');
33
42
  const writePidFile = () => {
@@ -172,11 +181,13 @@ program
172
181
  const serviceRegistry = createServiceRegistry({ configPath: config.services.configPath });
173
182
  // Start credential proxy
174
183
  const socketPath = path.join(getConfigDir(), 'proxy.sock');
184
+ const policyConfig = loadPolicyFromConfig(config);
175
185
  const credentialProxy = createCredentialProxy({
176
186
  socketPath,
177
187
  store: credentialStore,
178
188
  allowedServices: config.credentials.proxiedServices,
179
189
  serviceRegistry,
190
+ policyConfig,
180
191
  onRequest: (info) => {
181
192
  auditLogger.logCredentialAccess('system', 'system', {
182
193
  service: info.service,
@@ -324,11 +335,13 @@ program
324
335
  // Initialize service registry
325
336
  const serviceRegistry = createServiceRegistry({ configPath: config.services.configPath });
326
337
  // Start credential proxy
338
+ const policyConfig2 = loadPolicyFromConfig(config);
327
339
  const credentialProxy = createCredentialProxy({
328
340
  socketPath,
329
341
  store: credentialStore,
330
342
  allowedServices: config.credentials.proxiedServices,
331
343
  serviceRegistry,
344
+ policyConfig: policyConfig2,
332
345
  onRequest: (info) => {
333
346
  auditLogger.logCredentialAccess('system', 'system', {
334
347
  service: info.service,
@@ -401,11 +414,13 @@ program
401
414
  // Initialize service registry
402
415
  const serviceRegistry = createServiceRegistry({ configPath: config.services.configPath });
403
416
  // Start credential proxy
417
+ const policyConfig3 = loadPolicyFromConfig(config);
404
418
  const credentialProxy = createCredentialProxy({
405
419
  socketPath,
406
420
  store: credentialStore,
407
421
  allowedServices: config.credentials.proxiedServices,
408
422
  serviceRegistry,
423
+ policyConfig: policyConfig3,
409
424
  onRequest: (info) => {
410
425
  auditLogger.logCredentialAccess('system', 'system', {
411
426
  service: info.service,
@@ -498,6 +513,7 @@ program
498
513
  .description('All-in-one setup wizard — creates config, stores credentials, installs plugin')
499
514
  .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
500
515
  .option('--no-openclaw', 'Skip OpenClaw plugin installation')
516
+ .option('--no-policy', 'Skip request policy preset configuration')
501
517
  .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
502
518
  .action(async (options) => {
503
519
  const os = await import('node:os');
@@ -769,6 +785,43 @@ program
769
785
  console.log(' Skipped\n');
770
786
  }
771
787
  }
788
+ // 3.5. Apply request policy presets
789
+ if (options.policy !== false && storedServices.length > 0) {
790
+ let shouldApplyPolicy = true;
791
+ if (!isNonInteractive) {
792
+ const rl = (await import('node:readline')).createInterface({
793
+ input: process.stdin,
794
+ output: process.stdout
795
+ });
796
+ const answer = await new Promise((resolve) => {
797
+ rl.question(' ? Enable API request policies? (Y/n): ', resolve);
798
+ });
799
+ rl.close();
800
+ shouldApplyPolicy = answer.toLowerCase() !== 'n';
801
+ if (shouldApplyPolicy) {
802
+ console.log(' Policies restrict which API endpoints agents can call.\n');
803
+ }
804
+ }
805
+ if (shouldApplyPolicy) {
806
+ const presets = getDefaultPolicyPresets();
807
+ const policyToApply = {};
808
+ for (const svc of storedServices) {
809
+ if (presets[svc]) {
810
+ policyToApply[svc] = presets[svc];
811
+ }
812
+ }
813
+ if (Object.keys(policyToApply).length > 0) {
814
+ config.policy = policyToApply;
815
+ saveConfig(config);
816
+ console.log(' Applying default policy presets:\n');
817
+ for (const [svc, sp] of Object.entries(policyToApply)) {
818
+ console.log(formatServicePolicy(svc, sp, ' '));
819
+ console.log('');
820
+ }
821
+ console.log(' Customize policies later: ~/.aquaman/config.yaml\n');
822
+ }
823
+ }
824
+ }
772
825
  // 4. Detect OpenClaw and install plugin
773
826
  if (options.openclaw !== false) {
774
827
  const openclawDetected = fs.existsSync(openclawStateDir);
@@ -1060,6 +1113,42 @@ program
1060
1113
  }
1061
1114
  }
1062
1115
  }
1116
+ // 5.5 Policy config
1117
+ if (config?.policy && Object.keys(config.policy).length > 0) {
1118
+ const policyConfigDoc = loadPolicyFromConfig(config);
1119
+ const validation = validatePolicyConfig(policyConfigDoc);
1120
+ if (validation.valid) {
1121
+ const serviceCount = Object.keys(policyConfigDoc).length;
1122
+ const ruleCount = Object.values(policyConfigDoc).reduce((sum, sp) => sum + sp.rules.length, 0);
1123
+ console.log(` \u2713 ${aqua('Policy')} valid (${serviceCount} service${serviceCount !== 1 ? 's' : ''}, ${ruleCount} rule${ruleCount !== 1 ? 's' : ''})`);
1124
+ for (const [svc, sp] of Object.entries(policyConfigDoc)) {
1125
+ const denyRules = sp.rules.filter(r => r.action === 'deny');
1126
+ if (denyRules.length > 0) {
1127
+ const summary = denyRules.map(r => r.method !== '*' ? `${r.action} ${r.method} ${r.path}` : `${r.action} ${r.path}`).join(', ');
1128
+ console.log(` ${svc}: ${summary}`);
1129
+ }
1130
+ }
1131
+ // Warn about policies for non-proxied services
1132
+ if (config.credentials.proxiedServices) {
1133
+ for (const svc of Object.keys(policyConfigDoc)) {
1134
+ if (!config.credentials.proxiedServices.includes(svc)) {
1135
+ console.log(` \u26a0 ${aqua('Policy')} service "${svc}" has rules but is not in proxiedServices`);
1136
+ issues++;
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+ else {
1142
+ for (const err of validation.errors) {
1143
+ console.log(` \u2717 ${aqua('Policy')} ${err}`);
1144
+ }
1145
+ issues++;
1146
+ }
1147
+ }
1148
+ else {
1149
+ console.log(` \u2139 ${aqua('Policy')} not configured \u2014 agents can call any endpoint on proxied services`);
1150
+ console.log(' \u2192 Run: aquaman setup (or add policy rules to ~/.aquaman/config.yaml)');
1151
+ }
1063
1152
  // 6. OpenClaw detection
1064
1153
  let openclawDetected = false;
1065
1154
  let cliFound = false;
@@ -1323,6 +1412,8 @@ credentials
1323
1412
  .description('Add a credential')
1324
1413
  .option('--backend <backend>', 'Override credential backend')
1325
1414
  .action(async (service, key, options) => {
1415
+ validateCredName('service', service);
1416
+ validateCredName('key', key);
1326
1417
  const config = loadConfig();
1327
1418
  const backend = options.backend || config.credentials.backend;
1328
1419
  let store;
@@ -1418,6 +1509,8 @@ credentials
1418
1509
  .command('delete <service> <key>')
1419
1510
  .description('Delete a credential')
1420
1511
  .action(async (service, key) => {
1512
+ validateCredName('service', service);
1513
+ validateCredName('key', key);
1421
1514
  const config = loadConfig();
1422
1515
  let store;
1423
1516
  try {
@@ -1572,6 +1665,42 @@ services
1572
1665
  process.exit(1);
1573
1666
  }
1574
1667
  });
1668
+ // Policy commands
1669
+ const policy = program.command('policy').description('Request policy management');
1670
+ policy
1671
+ .command('list')
1672
+ .description('List configured policy rules')
1673
+ .action(async () => {
1674
+ const config = loadConfig();
1675
+ const policyConfig = loadPolicyFromConfig(config);
1676
+ if (Object.keys(policyConfig).length === 0) {
1677
+ console.log('No policies configured. Run: aquaman setup');
1678
+ return;
1679
+ }
1680
+ for (const [name, sp] of Object.entries(policyConfig)) {
1681
+ console.log(formatServicePolicy(name, sp));
1682
+ }
1683
+ });
1684
+ policy
1685
+ .command('test <service> <method> <path>')
1686
+ .description('Test whether a request would be allowed or denied')
1687
+ .action(async (service, method, reqPath) => {
1688
+ const config = loadConfig();
1689
+ const policyConfig = loadPolicyFromConfig(config);
1690
+ const result = matchPolicy(service, method.toUpperCase(), reqPath, policyConfig);
1691
+ if (!policyConfig[service]) {
1692
+ console.log(` \u2713 ALLOWED (no policy for service "${service}")`);
1693
+ return;
1694
+ }
1695
+ if (result.allowed) {
1696
+ const svcPolicy = policyConfig[service];
1697
+ console.log(` \u2713 ALLOWED (no matching rule, default: ${svcPolicy.defaultAction})`);
1698
+ }
1699
+ else {
1700
+ const rule = result.matchedRule;
1701
+ console.log(` \u2717 DENIED by rule: ${rule.method} ${rule.path} \u2192 ${rule.action}`);
1702
+ }
1703
+ });
1575
1704
  // Migration commands
1576
1705
  const migrate = program.command('migrate').description('Migrate credentials from other sources');
1577
1706
  migrate
@@ -1979,6 +2108,19 @@ program
1979
2108
  for (const service of config.credentials.proxiedServices) {
1980
2109
  console.log(` - ${service}`);
1981
2110
  }
2111
+ // Policy summary
2112
+ const policyConfig = loadPolicyFromConfig(config);
2113
+ const policySvcCount = Object.keys(policyConfig).length;
2114
+ if (policySvcCount > 0) {
2115
+ const ruleCount = Object.values(policyConfig).reduce((sum, sp) => sum + sp.rules.length, 0);
2116
+ console.log(`\nRequest policies: ${policySvcCount} service${policySvcCount !== 1 ? 's' : ''}, ${ruleCount} rule${ruleCount !== 1 ? 's' : ''}`);
2117
+ for (const [svc, sp] of Object.entries(policyConfig)) {
2118
+ console.log(` - ${svc}: ${sp.rules.length} rule${sp.rules.length !== 1 ? 's' : ''} (default: ${sp.defaultAction})`);
2119
+ }
2120
+ }
2121
+ else {
2122
+ console.log('\nRequest policies: not configured');
2123
+ }
1982
2124
  // Check for stored credentials
1983
2125
  try {
1984
2126
  const store = await createCredentialStore({
@@ -2007,6 +2149,16 @@ program
2007
2149
  const info = await integration.detectOpenClaw();
2008
2150
  console.log(`\nOpenClaw: ${info.installed ? `installed (${info.version})` : 'not found'}`);
2009
2151
  });
2152
+ /** Format a single service policy for display (used by policy list, setup, doctor) */
2153
+ function formatServicePolicy(name, sp, indent = ' ') {
2154
+ const lines = [];
2155
+ lines.push(`${indent}${name} (default: ${sp.defaultAction})`);
2156
+ for (const rule of sp.rules) {
2157
+ const method = rule.method.padEnd(6);
2158
+ lines.push(`${indent} ${rule.action === 'deny' ? 'deny' : 'allow'} ${method} ${rule.path}`);
2159
+ }
2160
+ return lines.join('\n');
2161
+ }
2010
2162
  function formatEntry(entry) {
2011
2163
  switch (entry.type) {
2012
2164
  case 'tool_call':
@@ -2014,7 +2166,7 @@ function formatEntry(entry) {
2014
2166
  case 'tool_result':
2015
2167
  return `Result for ${entry.data.toolCallId}`;
2016
2168
  case 'credential_access':
2017
- return `${entry.data.service} ${entry.data.operation} ${entry.data.success ? 'OK' : 'FAIL'}`;
2169
+ return `${entry.data.service} ${entry.data.operation} ${entry.data.success ? 'OK' : `FAIL: ${entry.data.error || 'unknown'}`}`;
2018
2170
  default:
2019
2171
  return JSON.stringify(entry.data).slice(0, 80);
2020
2172
  }