@sandprivacy/sandgate 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +13 -3
- package/dist/passphrase.js +29 -0
- package/dist/test/passphrase.test.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ How the trust works: the pairing secret travels once, inside the URL **fragment*
|
|
|
90
90
|
|
|
91
91
|
## Security notes, honestly
|
|
92
92
|
|
|
93
|
-
-
|
|
93
|
+
- MCP clients launch servers non-interactively, so the vault passphrase must come from the environment. `SANDGATE_PASSPHRASE` (the value, cleartext in your config) protects the vault *at rest* — a stolen `vault.enc` alone is useless. For more, `SANDGATE_PASSPHRASE_CMD` runs a command whose stdout is the passphrase, so it can live in your OS secret store: Windows DPAPI (`ConvertFrom-SecureString` once, decrypt in the command), macOS `security find-generic-password`, Linux `secret-tool lookup`, or any password manager CLI. Either way, a fully compromised machine defeats any local secret store — that threat class is out of scope for all of them.
|
|
94
94
|
- Approval taps are only accepted from your own Telegram chat; anything else — including silence — is a deny. Agent-supplied text in approval messages is escaped and truncated.
|
|
95
95
|
- Email content handled by `wait_for_verification` is untrusted third-party input. The tool description tells agents so; only the extracted code and hint-filtered links are returned, never the raw body.
|
|
96
96
|
- Several agents can wait on you at once: approvals are served by a single dispatcher, first tap wins per request.
|
|
@@ -104,7 +104,7 @@ How the trust works: the pairing secret travels once, inside the URL **fragment*
|
|
|
104
104
|
- [ ] OS keychain for the vault passphrase
|
|
105
105
|
- [ ] Slack approval channel with multiple approvers (teams)
|
|
106
106
|
- [ ] Team policies (shared vault, centralized audit)
|
|
107
|
-
- [
|
|
107
|
+
- [x] Framework guides: [Claude Code](docs/integrations/claude-code.md), [browser-use](docs/integrations/browser-use.md), [Playwright MCP](docs/integrations/playwright-mcp.md), [LangGraph](docs/integrations/langgraph.md)
|
|
108
108
|
|
|
109
109
|
## License
|
|
110
110
|
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { read } from "read";
|
|
6
6
|
import { getQuota } from "./sandmail.js";
|
|
7
7
|
import { testImapConnection } from "./inbox.js";
|
|
8
|
+
import { resolvePassphrase } from "./passphrase.js";
|
|
8
9
|
import { auditPath } from "./paths.js";
|
|
9
10
|
import { vaultExists, loadVault, saveVault, } from "./vault.js";
|
|
10
11
|
import { loadConfig, saveConfig } from "./config.js";
|
|
@@ -391,10 +392,19 @@ async function main() {
|
|
|
391
392
|
return cmdAudit(args[0]);
|
|
392
393
|
case undefined:
|
|
393
394
|
case "serve": {
|
|
394
|
-
|
|
395
|
+
let pass;
|
|
396
|
+
try {
|
|
397
|
+
pass = resolvePassphrase(process.env);
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
console.error(`SANDGATE_PASSPHRASE_CMD failed: ${err instanceof Error ? err.message : err}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
395
403
|
if (!pass) {
|
|
396
|
-
console.error("
|
|
397
|
-
'
|
|
404
|
+
console.error("No vault passphrase. MCP clients launch sandgate non-interactively; provide either\n" +
|
|
405
|
+
' SANDGATE_PASSPHRASE the value itself, or\n' +
|
|
406
|
+
" SANDGATE_PASSPHRASE_CMD a command printing it (OS keychain, DPAPI, password manager CLI)\n" +
|
|
407
|
+
"in the MCP server config env.");
|
|
398
408
|
process.exit(1);
|
|
399
409
|
}
|
|
400
410
|
return serve(pass);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the vault passphrase for non-interactive runs (MCP clients
|
|
4
|
+
* launching `sandgate serve`). Two sources, in order:
|
|
5
|
+
*
|
|
6
|
+
* - SANDGATE_PASSPHRASE: the value itself. Simple, but sits in cleartext
|
|
7
|
+
* in whatever config launches the server.
|
|
8
|
+
* - SANDGATE_PASSPHRASE_CMD: a command whose stdout is the passphrase —
|
|
9
|
+
* the git-credential/restic pattern. Point it at the OS secret store:
|
|
10
|
+
* DPAPI on Windows, `security find-generic-password` on macOS,
|
|
11
|
+
* `secret-tool lookup` on Linux, or any password manager CLI.
|
|
12
|
+
*
|
|
13
|
+
* Either way the model never sees it: it lives in the launcher's
|
|
14
|
+
* environment, not in the agent's context.
|
|
15
|
+
*/
|
|
16
|
+
export function resolvePassphrase(env) {
|
|
17
|
+
if (env.SANDGATE_PASSPHRASE)
|
|
18
|
+
return env.SANDGATE_PASSPHRASE;
|
|
19
|
+
const command = env.SANDGATE_PASSPHRASE_CMD;
|
|
20
|
+
if (!command)
|
|
21
|
+
return undefined;
|
|
22
|
+
const output = execSync(command, {
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
timeout: 15_000,
|
|
26
|
+
});
|
|
27
|
+
const pass = output.trim();
|
|
28
|
+
return pass.length > 0 ? pass : undefined;
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolvePassphrase } from "../passphrase.js";
|
|
4
|
+
test("direct SANDGATE_PASSPHRASE wins", () => {
|
|
5
|
+
assert.equal(resolvePassphrase({ SANDGATE_PASSPHRASE: "direct", SANDGATE_PASSPHRASE_CMD: "echo nope" }), "direct");
|
|
6
|
+
});
|
|
7
|
+
test("SANDGATE_PASSPHRASE_CMD output is used, trimmed", () => {
|
|
8
|
+
const cmd = `node -e "console.log(' from-command ')"`;
|
|
9
|
+
assert.equal(resolvePassphrase({ SANDGATE_PASSPHRASE_CMD: cmd }), "from-command");
|
|
10
|
+
});
|
|
11
|
+
test("empty command output resolves to undefined", () => {
|
|
12
|
+
const cmd = `node -e "console.log('')"`;
|
|
13
|
+
assert.equal(resolvePassphrase({ SANDGATE_PASSPHRASE_CMD: cmd }), undefined);
|
|
14
|
+
});
|
|
15
|
+
test("neither variable set resolves to undefined", () => {
|
|
16
|
+
assert.equal(resolvePassphrase({}), undefined);
|
|
17
|
+
});
|
|
18
|
+
test("a failing command throws", () => {
|
|
19
|
+
assert.throws(() => resolvePassphrase({ SANDGATE_PASSPHRASE_CMD: `node -e "process.exit(3)"` }));
|
|
20
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandprivacy/sandgate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The human gateway for AI agents — approvals, 2FA codes and email verification, self-hosted. Your agent asks; you decide; secrets never touch the LLM.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|