@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 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
- - `SANDGATE_PASSPHRASE` in the MCP client config is a deliberate tradeoff: MCP clients launch servers non-interactively, so the passphrase lives in your agent's config file. It protects the vault *at rest* (a stolen `vault.enc` alone is useless); OS keychain integration is on the roadmap.
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
- - [ ] Framework guides: browser-use, LangGraph, Agno
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
- const pass = process.env.SANDGATE_PASSPHRASE;
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("SANDGATE_PASSPHRASE is not set. MCP clients launch sandgate non-interactively;\n" +
397
- 'add it to the server config, e.g. {"env": {"SANDGATE_PASSPHRASE": "..."}}');
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.0",
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",