@icoretech/warden-mcp 0.1.8 → 0.1.9
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 +22 -0
- package/dist/sdk/keychainSdk.js +4 -0
- package/dist/tools/registerTools.js +38 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -367,12 +367,14 @@ Credential resolution:
|
|
|
367
367
|
Mutation control:
|
|
368
368
|
|
|
369
369
|
- Set `READONLY=true` to block all write operations (create/edit/delete/move/restore/attachments).
|
|
370
|
+
- Set `NOREVEAL=true` to force all `reveal` parameters to `false` server-side. Clients can still request `reveal: true`, but the server will silently downgrade to redacted output. This prevents prompt injection from tricking an LLM agent into exfiltrating secrets.
|
|
370
371
|
- Session guardrails:
|
|
371
372
|
- `KEYCHAIN_SESSION_MAX_COUNT` (default `32`)
|
|
372
373
|
- `KEYCHAIN_SESSION_TTL_MS` (default `900000`)
|
|
373
374
|
- `KEYCHAIN_SESSION_SWEEP_INTERVAL_MS` (default `60000`)
|
|
374
375
|
- `KEYCHAIN_MAX_HEAP_USED_MB` (default `1536`, set `0` to disable memory fuse)
|
|
375
376
|
- `KEYCHAIN_METRICS_LOG_INTERVAL_MS` (default `0`, disabled)
|
|
377
|
+
- `NOREVEAL` / `KEYCHAIN_NOREVEAL` (default `false`; force all reveals to false)
|
|
376
378
|
- `KEYCHAIN_ALLOW_ENV_FALLBACK` (default `false`; HTTP env-var credential fallback)
|
|
377
379
|
|
|
378
380
|
Redaction defaults (item reads):
|
|
@@ -390,6 +392,26 @@ Reveal rules:
|
|
|
390
392
|
- Secret helper tools (`get_password`, `get_totp`, `get_notes`, `generate`, `get_password_history`) return `structuredContent.result = { kind, value, revealed }`.
|
|
391
393
|
- When `reveal` is omitted/false, `value` is `null` (or historic passwords are `null`) and `revealed: false`.
|
|
392
394
|
|
|
395
|
+
## Production Deployment Checklist
|
|
396
|
+
|
|
397
|
+
If you run `warden-mcp` beyond local development, review these items:
|
|
398
|
+
|
|
399
|
+
1. **TLS everywhere.** Always terminate TLS in front of the HTTP endpoint. `X-BW-*` headers carry master passwords in cleartext — without TLS they are visible to anyone on the network.
|
|
400
|
+
|
|
401
|
+
2. **Network isolation.** Bind the server to `127.0.0.1` or place it behind an authenticated reverse proxy. The service has no built-in authentication; anyone who can reach `/sse` can issue vault operations.
|
|
402
|
+
|
|
403
|
+
3. **Do not enable `KEYCHAIN_ALLOW_ENV_FALLBACK` on shared networks.** This flag makes the server's own vault credentials available to any HTTP client that omits headers. Only use it in single-tenant setups where the network is fully trusted.
|
|
404
|
+
|
|
405
|
+
4. **Enable `READONLY=true` when writes are not needed.** This blocks all mutating tools at the MCP layer, limiting blast radius if an agent or client is compromised.
|
|
406
|
+
|
|
407
|
+
5. **Restrict filesystem access to `/data/bw-profiles`.** The `bw` CLI stores decrypted state under its HOME directory. Ensure the profile directory is not world-readable and is mounted with appropriate permissions (the Docker image runs as non-root by default).
|
|
408
|
+
|
|
409
|
+
6. **Disable debug logging in production.** `KEYCHAIN_DEBUG_BW` and `KEYCHAIN_DEBUG_HTTP` emit request details and CLI invocations to stdout. Debug logs may include session metadata and request structure. Keep them off unless actively troubleshooting.
|
|
410
|
+
|
|
411
|
+
7. **Set `NOREVEAL=true` when secrets should never leave the server.** This forces all `reveal` parameters to `false` server-side, regardless of what the client requests. Use this when the MCP host is an LLM agent that could be influenced by prompt injection — it prevents tricked agents from exfiltrating passwords or TOTP codes.
|
|
412
|
+
|
|
413
|
+
8. **Monitor `/metricsz`.** The endpoint is intentionally unauthenticated (for scraper compatibility) but exposes session counts, heap usage, and rejection counters. If this data is sensitive in your environment, restrict access at the network level.
|
|
414
|
+
|
|
393
415
|
## Quick Start
|
|
394
416
|
|
|
395
417
|
### Minimal local run
|
package/dist/sdk/keychainSdk.js
CHANGED
|
@@ -418,6 +418,10 @@ export class KeychainSdk {
|
|
|
418
418
|
});
|
|
419
419
|
}
|
|
420
420
|
async receive(input) {
|
|
421
|
+
const parsed = new URL(input.url);
|
|
422
|
+
if (parsed.protocol !== 'https:') {
|
|
423
|
+
throw new Error('receive URL must use HTTPS');
|
|
424
|
+
}
|
|
421
425
|
return this.bw.withSession(async (session) => {
|
|
422
426
|
const opts = ['receive'];
|
|
423
427
|
if (typeof input.password === 'string')
|
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
export function registerTools(server, deps) {
|
|
4
4
|
const toolMeta = {};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
function parseBoolEnv(...names) {
|
|
6
|
+
for (const name of names) {
|
|
7
|
+
const v = process.env[name];
|
|
8
|
+
if (v !== undefined) {
|
|
9
|
+
const lower = v.trim().toLowerCase();
|
|
10
|
+
if (lower === '1' ||
|
|
11
|
+
lower === 'true' ||
|
|
12
|
+
lower === 'yes' ||
|
|
13
|
+
lower === 'on')
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const isReadOnly = parseBoolEnv('READONLY', 'KEYCHAIN_READONLY');
|
|
20
|
+
const isNoReveal = parseBoolEnv('NOREVEAL', 'KEYCHAIN_NOREVEAL');
|
|
11
21
|
function readonlyBlocked() {
|
|
12
22
|
return {
|
|
13
23
|
structuredContent: { ok: false, error: 'READONLY' },
|
|
@@ -15,6 +25,16 @@ export function registerTools(server, deps) {
|
|
|
15
25
|
isError: true,
|
|
16
26
|
};
|
|
17
27
|
}
|
|
28
|
+
/** Strip reveal from input when NOREVEAL is set. */
|
|
29
|
+
function clampReveal(input) {
|
|
30
|
+
if (isNoReveal && input.reveal) {
|
|
31
|
+
return { ...input, reveal: false };
|
|
32
|
+
}
|
|
33
|
+
return input;
|
|
34
|
+
}
|
|
35
|
+
function effectiveReveal(input) {
|
|
36
|
+
return isNoReveal ? false : (input.reveal ?? false);
|
|
37
|
+
}
|
|
18
38
|
function toolResult(kind, value, revealed) {
|
|
19
39
|
return { result: { kind, value, revealed } };
|
|
20
40
|
}
|
|
@@ -120,7 +140,7 @@ export function registerTools(server, deps) {
|
|
|
120
140
|
_meta: toolMeta,
|
|
121
141
|
}, async (input, extra) => {
|
|
122
142
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
123
|
-
const result = await sdk.generate(input);
|
|
143
|
+
const result = await sdk.generate(clampReveal(input));
|
|
124
144
|
return {
|
|
125
145
|
structuredContent: toolResult('generated', result.value, result.revealed),
|
|
126
146
|
content: [{ type: 'text', text: 'OK' }],
|
|
@@ -148,7 +168,7 @@ export function registerTools(server, deps) {
|
|
|
148
168
|
_meta: toolMeta,
|
|
149
169
|
}, async (input, extra) => {
|
|
150
170
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
151
|
-
const result = await sdk.generateUsername(input);
|
|
171
|
+
const result = await sdk.generateUsername(clampReveal(input));
|
|
152
172
|
return {
|
|
153
173
|
structuredContent: toolResult('generated', result.value, result.revealed),
|
|
154
174
|
content: [{ type: 'text', text: 'OK' }],
|
|
@@ -427,7 +447,9 @@ export function registerTools(server, deps) {
|
|
|
427
447
|
_meta: toolMeta,
|
|
428
448
|
}, async (input, extra) => {
|
|
429
449
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
430
|
-
const item = await sdk.getItem(input.id, {
|
|
450
|
+
const item = await sdk.getItem(input.id, {
|
|
451
|
+
reveal: effectiveReveal(input),
|
|
452
|
+
});
|
|
431
453
|
return {
|
|
432
454
|
structuredContent: { item },
|
|
433
455
|
content: [{ type: 'text', text: 'OK' }],
|
|
@@ -460,7 +482,7 @@ export function registerTools(server, deps) {
|
|
|
460
482
|
_meta: toolMeta,
|
|
461
483
|
}, async (input, extra) => {
|
|
462
484
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
463
|
-
const notes = await sdk.getNotes({ term: input.term }, { reveal: input
|
|
485
|
+
const notes = await sdk.getNotes({ term: input.term }, { reveal: effectiveReveal(input) });
|
|
464
486
|
return {
|
|
465
487
|
structuredContent: toolResult('notes', notes.value, notes.revealed),
|
|
466
488
|
content: [{ type: 'text', text: 'OK' }],
|
|
@@ -618,7 +640,7 @@ export function registerTools(server, deps) {
|
|
|
618
640
|
if (isReadOnly)
|
|
619
641
|
return readonlyBlocked();
|
|
620
642
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
621
|
-
const item = await sdk.createAttachment(input);
|
|
643
|
+
const item = await sdk.createAttachment(clampReveal(input));
|
|
622
644
|
return {
|
|
623
645
|
structuredContent: { item },
|
|
624
646
|
content: [{ type: 'text', text: 'Attached.' }],
|
|
@@ -637,7 +659,7 @@ export function registerTools(server, deps) {
|
|
|
637
659
|
if (isReadOnly)
|
|
638
660
|
return readonlyBlocked();
|
|
639
661
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
640
|
-
const item = await sdk.deleteAttachment(input);
|
|
662
|
+
const item = await sdk.deleteAttachment(clampReveal(input));
|
|
641
663
|
return {
|
|
642
664
|
structuredContent: { item },
|
|
643
665
|
content: [{ type: 'text', text: 'Deleted.' }],
|
|
@@ -885,7 +907,7 @@ export function registerTools(server, deps) {
|
|
|
885
907
|
_meta: toolMeta,
|
|
886
908
|
}, async (input, extra) => {
|
|
887
909
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
888
|
-
const password = await sdk.getPassword({ term: input.term }, { reveal: input
|
|
910
|
+
const password = await sdk.getPassword({ term: input.term }, { reveal: effectiveReveal(input) });
|
|
889
911
|
return {
|
|
890
912
|
structuredContent: toolResult('password', password.value, password.revealed),
|
|
891
913
|
content: [{ type: 'text', text: 'OK' }],
|
|
@@ -902,7 +924,7 @@ export function registerTools(server, deps) {
|
|
|
902
924
|
_meta: toolMeta,
|
|
903
925
|
}, async (input, extra) => {
|
|
904
926
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
905
|
-
const totp = await sdk.getTotp({ term: input.term }, { reveal: input
|
|
927
|
+
const totp = await sdk.getTotp({ term: input.term }, { reveal: effectiveReveal(input) });
|
|
906
928
|
return {
|
|
907
929
|
structuredContent: toolResult('totp', totp.value, totp.revealed),
|
|
908
930
|
content: [{ type: 'text', text: 'OK' }],
|
|
@@ -920,7 +942,7 @@ export function registerTools(server, deps) {
|
|
|
920
942
|
}, async (input, extra) => {
|
|
921
943
|
const sdk = await deps.getSdk(extra.authInfo);
|
|
922
944
|
const history = await sdk.getPasswordHistory(input.id, {
|
|
923
|
-
reveal: input
|
|
945
|
+
reveal: effectiveReveal(input),
|
|
924
946
|
});
|
|
925
947
|
return {
|
|
926
948
|
structuredContent: toolResult('password_history', history.value, history.revealed),
|
|
@@ -1050,7 +1072,7 @@ export function registerTools(server, deps) {
|
|
|
1050
1072
|
id: input.id,
|
|
1051
1073
|
mode: input.mode,
|
|
1052
1074
|
uris: normalizeUrisInput(input.uris) ?? [],
|
|
1053
|
-
reveal: input
|
|
1075
|
+
reveal: effectiveReveal(input),
|
|
1054
1076
|
});
|
|
1055
1077
|
return {
|
|
1056
1078
|
structuredContent: { item: updated },
|