@suveren/gateway 0.19.0 → 0.19.2

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
@@ -142,7 +142,22 @@ For laptops managed by a company's IT: no admin rights, no Node.js and no access
142
142
 
143
143
  **The installer is not signed by us.** IT checks where it comes from (`sha256sum -c SHA256SUMS` and `gh attestation verify <file> -R suverenai/suveren-gateway`, which shows the exact commit it was built from), signs it, and distributes it, e.g. with Intune. IT can preset the Authority Server, simulation mode, proxy and certificate centrally; the employee cannot change them, and updates come from IT. Step by step: [Windows IT guide](docs/windows-it-guide.md) · settings: [Managed settings](docs/managed-settings.md).
144
144
 
145
- Open `http://localhost:3400` (Start menu: *Suveren Gateway*). The MCP server is at `http://localhost:3430`.
145
+ Open `http://localhost:3400` (Start menu: *Suveren Gateway*). The MCP server is at `http://localhost:3430`. Other ports or another data folder: `msiexec /i suveren-gateway.msi PORT=3500 MCP_PORT=3530 DATA_DIR="D:\Suveren"`, or the "Gateway settings" page of a double-click install — see the [IT guide](docs/windows-it-guide.md#port-and-data-folder-optional).
146
+
147
+ ### Ports and data folder
148
+
149
+ npm and the Windows installer default to port 3400 (app), 3430 (AI assistants) and the data folder `~/.suveren`. To change them for good — kept across restarts, upgrades and autostart:
150
+
151
+ ```bash
152
+ suveren-gateway stop # if you started it with --detach
153
+ suveren-gateway config set port 3500
154
+ suveren-gateway config set mcp-port 3530
155
+ suveren-gateway config set data-dir /Volumes/Data/suveren
156
+ suveren-gateway config get # shows each value and where it comes from
157
+ suveren-gateway config unset port # back to the default
158
+ ```
159
+
160
+ Changing the data folder **does not move** existing data — copy the old folder yourself first if you want to keep it. After changing `mcp-port`, update the MCP address in every AI assistant you connected. Environment variables (`SUVEREN_CP_PORT`, `SUVEREN_MCP_PORT`, `SUVEREN_DATA_DIR`) still win over saved values; IT policy wins over both ([Managed settings](docs/managed-settings.md)).
146
161
 
147
162
  ### Connecting an MCP client
148
163
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { spawn, spawnSync } from 'node:child_process';
14
14
  import { createConnection } from 'node:net';
15
- import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
15
+ import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
16
16
  import { homedir, platform, userInfo } from 'node:os';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -22,6 +22,7 @@ import { createInterface } from 'node:readline/promises';
22
22
  import { readPairing as readAsPairing, recordTlsPin, formatFingerprint, normalizeFingerprint } from '../lib/as-pairing.mjs';
23
23
  import { unsupportedNodeReason } from '../lib/node-version.mjs';
24
24
  import { readPolicy, isPolicyLocked } from '../lib/policy.mjs';
25
+ import { resolveInstallSettings, describeSource, installSettingsLocation, validatePort, validateDataDir, writeInstallSettings } from '../lib/install-settings.mjs';
25
26
 
26
27
  // Before any command: on an unsupported Node the connectors cannot run (see
27
28
  // lib/node-version.mjs), so refuse with the reason instead of starting.
@@ -35,11 +36,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
35
36
  const PKG_ROOT = resolve(__dirname, '..');
36
37
  const SERVER_ENTRY = join(PKG_ROOT, 'server.js');
37
38
 
38
- const DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join(homedir(), '.suveren');
39
+ // Ports + data folder: IT policy > env > saved install setting > default
40
+ // (lib/install-settings.mjs — the same resolver server.js uses, so the CLI
41
+ // and the running gateway always agree). An invalid value stops every
42
+ // command except `config` and `help`, which stay usable so the person can
43
+ // see and fix it.
44
+ const COMMAND = process.argv[2] ?? 'help';
45
+ const TOLERANT = ['config', 'help', '--help', '-h'].includes(COMMAND);
46
+ let INSTALL;
47
+ try {
48
+ INSTALL = resolveInstallSettings({ tolerant: TOLERANT });
49
+ } catch (err) {
50
+ console.error(err instanceof Error ? err.message : String(err));
51
+ console.error('Fix it with `suveren-gateway config set …` — see `suveren-gateway config help`.');
52
+ process.exit(1);
53
+ }
54
+ for (const warning of INSTALL.errors) console.error(`Warning: ${warning}`);
55
+
56
+ const DATA_DIR = INSTALL.dataDir;
39
57
  const PID_FILE = join(DATA_DIR, 'gateway.pid');
40
58
  const LOG_FILE = join(DATA_DIR, 'gateway.log');
41
59
 
42
- const SUVEREN_PORT = process.env.SUVEREN_CP_PORT ?? '3400';
60
+ const SUVEREN_PORT = String(INSTALL.cpPort);
61
+ const MCP_PORT = String(INSTALL.mcpPort);
43
62
 
44
63
  /** Version of THIS CLI (the binary on disk). Compared against the
45
64
  * running gateway's version inside `status` so users see a mismatch
@@ -267,11 +286,21 @@ async function start(args) {
267
286
  console.error(`another terminal, which leaves no PID file for this CLI to find.`);
268
287
  console.error(``);
269
288
  console.error(` Check what it is: suveren-gateway status`);
270
- console.error(` Or use another port: SUVEREN_CP_PORT=3410 suveren-gateway start`);
289
+ console.error(` Or use another port: suveren-gateway config set port 3410`);
271
290
  }
272
291
  process.exit(1);
273
292
  }
274
293
 
294
+ // The MCP port too: before this check, a taken MCP port let the gateway
295
+ // "start", then its MCP server died on EADDRINUSE and took the rest down
296
+ // with it — reported only in the log.
297
+ if (await isPortListening(MCP_PORT)) {
298
+ console.error(`Port ${MCP_PORT} (for AI assistants, MCP) is already in use.`);
299
+ console.error(``);
300
+ console.error(` Use another port: suveren-gateway config set mcp-port 3440`);
301
+ process.exit(1);
302
+ }
303
+
275
304
  ensureDataDir();
276
305
 
277
306
  // NOW save — a start we know is actually going to happen. Also override
@@ -543,15 +572,26 @@ function printConfigHelp() {
543
572
  console.log(`suveren-gateway config — read or save gateway settings
544
573
 
545
574
  Usage:
546
- suveren-gateway config get [as-url|ca-file|pin-tls|proxy] Print the resolved value(s)
575
+ suveren-gateway config get [as-url|ca-file|pin-tls|proxy|port|mcp-port|data-dir]
576
+ Print the resolved value(s)
547
577
  suveren-gateway config set as-url <url> Save the Authority Server URL
548
578
  suveren-gateway config set ca-file <path> Save a CA bundle for internal TLS
549
579
  suveren-gateway config set pin-tls on --expect-fingerprint <sha256-hex>
550
580
  Pin the Authority Server's TLS certificate
551
581
  suveren-gateway config set pin-tls off Stop enforcing the TLS certificate pin
552
582
  suveren-gateway config set proxy <url> Save an HTTP(S) proxy (http(s)://host:port)
553
-
554
- Saved in ${join(DATA_DIR, 'config.json')}.
583
+ suveren-gateway config set port <port> Port for the gateway app in the browser (default 3400)
584
+ suveren-gateway config set mcp-port <port> Port AI assistants connect to (default 3430)
585
+ suveren-gateway config set data-dir <folder> Folder for the vault, mandates and logs (default ~/.suveren)
586
+ suveren-gateway config unset port|mcp-port|data-dir Back to the default
587
+
588
+ Saved in ${join(DATA_DIR, 'config.json')} — except port, mcp-port and
589
+ data-dir, which live outside the data folder, in ${installSettingsLocation()}.
590
+ They are kept across restarts and autostart. Ports must be 1024–65535 and
591
+ differ from each other; the data folder must be a full path. Changing the
592
+ data folder does NOT move existing data. Precedence: IT policy > env var
593
+ (SUVEREN_CP_PORT / SUVEREN_MCP_PORT / SUVEREN_DATA_DIR) > saved > default.
594
+ Stop a gateway started with --detach before changing them.
555
595
  Precedence at start: IT policy > --as-url flag > env SUVEREN_AS_URL > saved
556
596
  as-url > default (${DEFAULT_AS_URL}).
557
597
 
@@ -631,6 +671,106 @@ function refuseIfLockedByPolicy(key, humanName, display) {
631
671
  process.exit(1);
632
672
  }
633
673
 
674
+ // ─── config: ports + data folder (lib/install-settings.mjs) ─────────────
675
+
676
+ /** `config` key → resolved-settings field, policy key and env var. */
677
+ const INSTALL_KEYS = {
678
+ port: { field: 'cpPort', policyKey: 'port', env: 'SUVEREN_CP_PORT', name: 'The gateway port' },
679
+ 'mcp-port': { field: 'mcpPort', policyKey: 'mcpPort', env: 'SUVEREN_MCP_PORT', name: 'The MCP port' },
680
+ 'data-dir': { field: 'dataDir', policyKey: 'dataDir', env: 'SUVEREN_DATA_DIR', name: 'The data folder' },
681
+ };
682
+
683
+ /**
684
+ * `config set port|mcp-port|data-dir <value>` and `config unset <key>`
685
+ * (`value` null). Saved outside the data folder — see install-settings.mjs.
686
+ */
687
+ async function setInstallSetting(key, value) {
688
+ const { field, policyKey, env, name } = INSTALL_KEYS[key];
689
+ refuseIfLockedByPolicy(policyKey, key);
690
+ const clearing = value === null;
691
+
692
+ let next;
693
+ if (!clearing) {
694
+ if (!value) {
695
+ console.error(`Usage: suveren-gateway config set ${key} <${key === 'data-dir' ? 'folder' : 'port'}>`);
696
+ process.exit(2);
697
+ }
698
+ const v = field === 'dataDir' ? validateDataDir(value) : validatePort(value);
699
+ if (!v.ok) {
700
+ console.error(`Invalid ${key}: ${v.error}`);
701
+ process.exit(1);
702
+ }
703
+ next = field === 'dataDir' ? v.path : v.port;
704
+ const other = field === 'cpPort' ? 'mcpPort' : field === 'mcpPort' ? 'cpPort' : null;
705
+ if (other && INSTALL[other] === next) {
706
+ console.error(`Invalid ${key}: ${next} is already the ${other === 'cpPort' ? 'gateway port' : 'MCP port'} — the two must differ.`);
707
+ process.exit(1);
708
+ }
709
+ }
710
+
711
+ // A gateway this CLI started with --detach keeps its PID file in the
712
+ // CURRENT data folder and listens on the CURRENT ports. Changing either
713
+ // underneath it leaves `stop`/`restart` unable to find it, and a second
714
+ // gateway fighting it for the port. Stop first. (A login-service gateway
715
+ // is fine: `restart` finds that one through the service manager.)
716
+ if (await isAlreadyRunning()) {
717
+ console.error(`The gateway is running (pid ${readPid()}). Stop it first, then change ${key}:`);
718
+ console.error(' suveren-gateway stop');
719
+ process.exit(1);
720
+ }
721
+
722
+ try {
723
+ writeInstallSettings({ [field]: clearing ? null : next });
724
+ } catch (err) {
725
+ console.error(err instanceof Error ? err.message : String(err));
726
+ process.exit(1);
727
+ }
728
+
729
+ const shown = clearing ? `back to the default` : `${next}`;
730
+ console.log(`Saved ${key}: ${shown} (in ${installSettingsLocation()})`);
731
+
732
+ if (process.env[env]) {
733
+ console.log(``);
734
+ console.log(` Note: ${env} is set in this shell (${process.env[env]}) and wins over the saved value.`);
735
+ }
736
+
737
+ if (field === 'mcpPort') {
738
+ const port = clearing ? 3430 : next;
739
+ console.log(``);
740
+ console.log(` AI assistants connect to the new address from the next start:`);
741
+ console.log(` http://localhost:${port}/mcp (SSE: http://localhost:${port}/sse)`);
742
+ console.log(` Update the MCP settings of every assistant you connected before.`);
743
+ }
744
+
745
+ if (field === 'dataDir') {
746
+ const newDir = clearing ? join(homedir(), '.suveren') : next;
747
+ const oldDir = INSTALL.dataDir;
748
+ let oldHasData = false;
749
+ try { oldHasData = resolve(newDir) !== resolve(oldDir) && readdirSync(oldDir).length > 0; } catch { /* no old folder */ }
750
+ if (oldHasData) {
751
+ console.log(``);
752
+ console.log(` Your existing data stays in ${oldDir} — nothing is moved or deleted.`);
753
+ console.log(` The gateway starts EMPTY in ${newDir}: you sign in and set up again.`);
754
+ console.log(` To keep your data instead, copy the folder there yourself before the next start.`);
755
+ }
756
+ // An autostart registered from a shell that had SUVEREN_DATA_DIR set
757
+ // carries it in the service definition, where it wins over this setting.
758
+ const unit = platform() === 'darwin' ? launchAgentPath() : platform() === 'linux' ? systemdUnitPath() : null;
759
+ try {
760
+ if (unit && existsSync(unit) && readFileSync(unit, 'utf8').includes('SUVEREN_DATA_DIR')) {
761
+ console.log(``);
762
+ console.log(` Your login service still sets SUVEREN_DATA_DIR itself (${unit}).`);
763
+ console.log(` Register it again from a shell without that variable: suveren-gateway service install`);
764
+ }
765
+ } catch { /* unreadable — nothing to warn about */ }
766
+ }
767
+
768
+ console.log(``);
769
+ console.log(serviceRunning()
770
+ ? ' Restart to apply it: suveren-gateway restart'
771
+ : ' It applies from the next start.');
772
+ }
773
+
634
774
  async function config(args) {
635
775
  const sub = args[0];
636
776
 
@@ -644,6 +784,14 @@ async function config(args) {
644
784
  console.log(`ca-file: ${(policyCaFile ?? saved.caFile) ?? '(not set)'}${policySuffix('caFile')}`);
645
785
  console.log(`pin-tls: ${resolvePinTls(DATA_DIR) ? 'on' : 'off'}${policySuffix('pinTls')}`);
646
786
  console.log(`proxy: ${(policyProxy ?? saved.proxyUrl) ?? '(not set)'}${policySuffix('proxy')}`);
787
+ console.log(`port: ${INSTALL.cpPort} (${describeSource(INSTALL.source.cpPort, 'cpPort')})`);
788
+ console.log(`mcp-port: ${INSTALL.mcpPort} (${describeSource(INSTALL.source.mcpPort, 'mcpPort')})`);
789
+ console.log(`data-dir: ${INSTALL.dataDir} (${describeSource(INSTALL.source.dataDir, 'dataDir')})`);
790
+ return;
791
+ }
792
+ const install = INSTALL_KEYS[key];
793
+ if (install) {
794
+ console.log(`${INSTALL[install.field]} (${describeSource(INSTALL.source[install.field], install.field)})`);
647
795
  return;
648
796
  }
649
797
  if (key === 'as-url') { console.log(`${resolveAsUrl(DATA_DIR)}${policySuffix('asUrl')}`); return; }
@@ -655,9 +803,18 @@ async function config(args) {
655
803
  process.exit(2);
656
804
  }
657
805
 
658
- if (sub === 'set') {
806
+ if (sub === 'set' || sub === 'unset') {
659
807
  const key = args[1];
660
808
  const value = args[2];
809
+ if (INSTALL_KEYS[key]) {
810
+ await setInstallSetting(key, sub === 'unset' ? null : value);
811
+ return;
812
+ }
813
+ if (sub === 'unset') {
814
+ console.error(`config unset works for port, mcp-port and data-dir only.\n`);
815
+ printConfigHelp();
816
+ process.exit(2);
817
+ }
661
818
  if (key === 'as-url') {
662
819
  refuseIfLockedByPolicy('asUrl', 'as-url');
663
820
  if (!value) {
@@ -1386,6 +1543,29 @@ async function verifyReport(args) {
1386
1543
  process.exitCode = await mod.runVerifyReportCli(args);
1387
1544
  }
1388
1545
 
1546
+ /**
1547
+ * `suveren-gateway open` — open the UI in the default browser, on whatever
1548
+ * port is configured. The Windows Start Menu entry and the installer's
1549
+ * Finish checkbox use this (via launcher.cmd) instead of a hard-coded port.
1550
+ */
1551
+ function openUi() {
1552
+ const url = `http://localhost:${SUVEREN_PORT}`;
1553
+ const os = platform();
1554
+ let child;
1555
+ if (os === 'win32') {
1556
+ // `start ""` — the empty title is required before a quoted target.
1557
+ // Verbatim, or Node escapes those quotes into something cmd misreads.
1558
+ child = spawn('cmd.exe', ['/d', '/s', '/c', `start "" "${url}"`], {
1559
+ detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true,
1560
+ });
1561
+ } else {
1562
+ child = spawn(os === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' });
1563
+ }
1564
+ child.on('error', () => { console.log(`Open ${url} in your browser.`); });
1565
+ child.unref();
1566
+ console.log(`Opening ${url}`);
1567
+ }
1568
+
1389
1569
  function printHelp() {
1390
1570
  console.log(`suveren-gateway — Suveren gateway (Human Agency Protocol)
1391
1571
 
@@ -1399,13 +1579,19 @@ Usage:
1399
1579
  suveren-gateway stop Stop a detached gateway
1400
1580
  suveren-gateway restart Stop, then start --detach
1401
1581
  suveren-gateway status Show running state + health (incl. simulation mode)
1582
+ suveren-gateway open Open the gateway in your browser
1402
1583
  suveren-gateway logs [--tail] Print or tail ~/.suveren/gateway.log
1403
1584
  suveren-gateway service <cmd> Run as a login service that survives reboot
1404
1585
  (install | uninstall | status)
1405
- suveren-gateway config get [as-url|ca-file|pin-tls|proxy] Print the resolved value(s)
1586
+ suveren-gateway config get [as-url|ca-file|pin-tls|proxy|port|mcp-port|data-dir]
1587
+ Print the resolved value(s)
1406
1588
  suveren-gateway config set as-url <url> Save the Authority Server URL
1407
1589
  suveren-gateway config set ca-file <path> Save a CA bundle for internal TLS
1408
1590
  suveren-gateway config set proxy <url> Save an HTTP(S) proxy (see below)
1591
+ suveren-gateway config set port <port> Save the gateway port (default 3400)
1592
+ suveren-gateway config set mcp-port <port> Save the port AI assistants connect to (default 3430)
1593
+ suveren-gateway config set data-dir <folder>
1594
+ Save the data folder (default ~/.suveren)
1409
1595
  suveren-gateway config set pin-tls on --expect-fingerprint <hex>
1410
1596
  Pin the Authority Server's TLS certificate
1411
1597
  (see \`suveren-gateway config help\` for the fingerprint check)
@@ -1419,11 +1605,15 @@ Usage:
1419
1605
  suveren-gateway help Print this help
1420
1606
 
1421
1607
  Environment:
1422
- SUVEREN_CP_PORT UI + API port (default 3400)
1423
- SUVEREN_MCP_PORT MCP server port (default 3430)
1424
- SUVEREN_DATA_DIR Data directory (default ~/.suveren)
1608
+ SUVEREN_CP_PORT UI + API port (default 3400) — overrides a saved \`config set port\`
1609
+ SUVEREN_MCP_PORT MCP server port (default 3430) — overrides a saved \`config set mcp-port\`
1610
+ SUVEREN_DATA_DIR Data directory (default ~/.suveren) — overrides a saved \`config set data-dir\`
1425
1611
  SUVEREN_AS_URL Authority Server URL — overrides the saved as-url
1426
1612
  SUVEREN_SIMULATION 1 to force simulation mode for this run — overrides the saved setting
1613
+ SUVEREN_BIND_HOST Listen address (default 127.0.0.1 — this machine only). Anything
1614
+ else lets other machines connect as your agent, so it needs:
1615
+ SUVEREN_MCP_TOKEN Token an AI assistant must send to open a session
1616
+ (Authorization: Bearer <token>, or ?token=<token>)
1427
1617
  HTTP_PROXY / HTTPS_PROXY / NO_PROXY (upper or lower case)
1428
1618
  Corporate proxy — honoured for every outbound call to the Authority
1429
1619
  Server, the update checker, and a remote AI assistant endpoint. A
@@ -1450,6 +1640,7 @@ async function main() {
1450
1640
  case 'stop': await stop(); break;
1451
1641
  case 'status': await status(); break;
1452
1642
  case 'restart': await restart(); break;
1643
+ case 'open': openUi(); break;
1453
1644
  case 'logs': await logs(argv.slice(1)); break;
1454
1645
  case 'service': await service(argv.slice(1)); break;
1455
1646
  case 'config': await config(argv.slice(1)); break;
@@ -9,7 +9,8 @@
9
9
  "npmVersion": "0.4.0",
10
10
  "mcp": {
11
11
  "command": "erp-mcp",
12
- "args": []
12
+ "args": [],
13
+ "passEnv": ["ERP_COMPANY_FILE"]
13
14
  },
14
15
  "credentials": {
15
16
  "fields": [
@@ -2786,6 +2786,21 @@ function requireAllowedHost(req, res, next) {
2786
2786
  next();
2787
2787
  }
2788
2788
 
2789
+ // src/lib/mcp-public-port.ts
2790
+ function mcpPublicPort(env = process.env, internalUrl) {
2791
+ for (const raw of [env.SUVEREN_MCP_PUBLIC_PORT, env.SUVEREN_MCP_PORT]) {
2792
+ const n = Number(raw);
2793
+ if (raw && Number.isInteger(n) && n > 0 && n <= 65535) return n;
2794
+ }
2795
+ try {
2796
+ const url = new URL(internalUrl ?? env.SUVEREN_MCP_INTERNAL_URL ?? "");
2797
+ const n = Number(url.port);
2798
+ if (Number.isInteger(n) && n > 0) return n;
2799
+ } catch {
2800
+ }
2801
+ return null;
2802
+ }
2803
+
2789
2804
  // src/lib/read-policy-defaults.ts
2790
2805
  async function backfillReadPolicyDefaults(data) {
2791
2806
  const defaults = /* @__PURE__ */ new Map();
@@ -5001,6 +5016,9 @@ app.get("/health", async (req, res) => {
5001
5016
  // purely so Settings can render "set by your IT" instead of controls
5002
5017
  // that would silently do nothing.
5003
5018
  policyLocked: POLICY_LOCKED,
5019
+ // Where AI assistants connect (the UI's setup snippet) — see
5020
+ // lib/mcp-public-port.ts for why the UI must not guess it.
5021
+ mcpPort: mcpPublicPort(process.env, MCP_BASE),
5004
5022
  session: buildSessionHealth(vault),
5005
5023
  security: {
5006
5024
  note: "Gateway secures tool execution. Agent host isolation is the user's responsibility."
@@ -5027,8 +5045,9 @@ if (existsSync11(UI_DIST)) {
5027
5045
  });
5028
5046
  });
5029
5047
  }
5030
- app.listen(port, "0.0.0.0", () => {
5031
- console.error(`[Control Plane] Listening on http://0.0.0.0:${port}`);
5048
+ var bindHost = process.env.SUVEREN_BIND_HOST?.trim() || "127.0.0.1";
5049
+ app.listen(port, bindHost, () => {
5050
+ console.error(`[Control Plane] Listening on http://${bindHost}:${port}`);
5032
5051
  if (process.env.SUVEREN_AUTOSTART === "1" && !vault.isUnlocked()) {
5033
5052
  const { title, message, url } = lockedNotification(port);
5034
5053
  notify(title, message, process.platform, url);
@@ -5192,6 +5192,60 @@ function safeHost(url) {
5192
5192
  }
5193
5193
  }
5194
5194
 
5195
+ // src/lib/connector-env.ts
5196
+ var RUNTIME_VARS = [
5197
+ // POSIX
5198
+ "HOME",
5199
+ "USER",
5200
+ "LOGNAME",
5201
+ "SHELL",
5202
+ "TERM",
5203
+ "TMPDIR",
5204
+ "LANG",
5205
+ "LC_ALL",
5206
+ "LC_CTYPE",
5207
+ "TZ",
5208
+ "XDG_CONFIG_HOME",
5209
+ "XDG_DATA_HOME",
5210
+ "XDG_CACHE_HOME",
5211
+ "XDG_RUNTIME_DIR",
5212
+ // Windows (names are case-insensitive there; matched case-insensitively below)
5213
+ "APPDATA",
5214
+ "LOCALAPPDATA",
5215
+ "USERPROFILE",
5216
+ "USERNAME",
5217
+ "HOMEDRIVE",
5218
+ "HOMEPATH",
5219
+ "SYSTEMROOT",
5220
+ "SYSTEMDRIVE",
5221
+ "WINDIR",
5222
+ "COMSPEC",
5223
+ "PATHEXT",
5224
+ "TEMP",
5225
+ "TMP",
5226
+ "PROGRAMFILES",
5227
+ "PROGRAMFILES(X86)",
5228
+ "PROGRAMDATA",
5229
+ "PROCESSOR_ARCHITECTURE",
5230
+ "NUMBER_OF_PROCESSORS"
5231
+ ];
5232
+ var NETWORK_VARS = [
5233
+ "HTTP_PROXY",
5234
+ "HTTPS_PROXY",
5235
+ "NO_PROXY",
5236
+ "NODE_EXTRA_CA_CERTS",
5237
+ "SSL_CERT_FILE"
5238
+ ];
5239
+ function inheritedConnectorEnv(parent, passEnv = []) {
5240
+ const allowed = new Set([...RUNTIME_VARS, ...NETWORK_VARS, ...passEnv].map((n) => n.toUpperCase()));
5241
+ const out = {};
5242
+ for (const [name, value] of Object.entries(parent)) {
5243
+ if (value === void 0) continue;
5244
+ if (allowed.has(name.toUpperCase())) out[name] = value;
5245
+ }
5246
+ return out;
5247
+ }
5248
+
5195
5249
  // src/lib/integration-manager.ts
5196
5250
  var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join14(homedir10(), ".suveren");
5197
5251
  var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join14(DEFAULT_DATA_DIR2, "integrations");
@@ -5585,7 +5639,8 @@ var IntegrationManager = class {
5585
5639
  if (envVar) env[envVar] = "simulation";
5586
5640
  }
5587
5641
  }
5588
- const interpolate = (s) => s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? process.env[name] ?? "");
5642
+ const inherited = inheritedConnectorEnv(process.env, getManifest(config.id)?.mcp.passEnv);
5643
+ const interpolate = (s) => s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? inherited[name] ?? "");
5589
5644
  const args = config.args.map(interpolate);
5590
5645
  const remote = remotePreflightTarget(config.command, args);
5591
5646
  if (remote) {
@@ -5595,13 +5650,13 @@ var IntegrationManager = class {
5595
5650
  const transport = new StdioClientTransport({
5596
5651
  command: config.command,
5597
5652
  args,
5598
- // `...process.env` is also how a saved `--ca-file` reaches an
5599
- // integration: NODE_EXTRA_CA_CERTS (read by Node only at process
5600
- // start) is already set on THIS process by bundle/server.js before
5601
- // the MCP server ever starts, so every integration this manager spawns
5602
- // inherits it here for free — no separate handling needed.
5653
+ // Only an allowlist of the gateway's environment — never its own
5654
+ // secrets (connector-env.ts). The allowlist includes
5655
+ // NODE_EXTRA_CA_CERTS, which is how a saved `--ca-file` reaches an
5656
+ // integration: bundle/server.js sets it on THIS process before the MCP
5657
+ // server starts, and Node reads it only at process start.
5603
5658
  env: {
5604
- ...process.env,
5659
+ ...inherited,
5605
5660
  PATH: buildPath(),
5606
5661
  HAP_DATA_DIR: DEFAULT_DATA_DIR2,
5607
5662
  ...config.env,
@@ -6275,6 +6330,73 @@ async function scopeReportSourcesToStoredWindow(base, stored, opts) {
6275
6330
  return { ok: true, window, sources: scopedSources(base, window, loadedAt) };
6276
6331
  }
6277
6332
 
6333
+ // src/lib/agent-access.ts
6334
+ import { timingSafeEqual } from "crypto";
6335
+ var DEFAULT_BIND_HOST = "127.0.0.1";
6336
+ function resolveBindHost(env = process.env) {
6337
+ const raw = env.SUVEREN_BIND_HOST?.trim();
6338
+ return raw ? raw : DEFAULT_BIND_HOST;
6339
+ }
6340
+ function isLoopbackBind(host) {
6341
+ const h = host.replace(/^\[|\]$/g, "").toLowerCase();
6342
+ return h === "localhost" || h === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
6343
+ }
6344
+ function resolveAgentToken(env = process.env) {
6345
+ return env.SUVEREN_MCP_TOKEN?.trim() ?? "";
6346
+ }
6347
+ function bindRefusal(env = process.env) {
6348
+ const host = resolveBindHost(env);
6349
+ if (isLoopbackBind(host)) return null;
6350
+ if (resolveAgentToken(env)) return null;
6351
+ if (env.SUVEREN_CONTAINER === "1") return null;
6352
+ return `SUVEREN_BIND_HOST=${host} would let other machines connect as this person's agent, but SUVEREN_MCP_TOKEN is not set. Set a token (long and random), or remove SUVEREN_BIND_HOST to listen on this machine only.`;
6353
+ }
6354
+ function hostnameOf(hostHeader) {
6355
+ const trimmed = hostHeader.trim().toLowerCase();
6356
+ const bracketed = trimmed.match(/^\[([^\]]+)\](?::\d+)?$/);
6357
+ if (bracketed) return bracketed[1];
6358
+ return trimmed.replace(/:\d+$/, "");
6359
+ }
6360
+ function isLocalHost(hostHeader) {
6361
+ if (!hostHeader) return false;
6362
+ const name = hostnameOf(hostHeader);
6363
+ if (name === "localhost" || name.endsWith(".localhost")) return true;
6364
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
6365
+ if (name.includes(":")) return true;
6366
+ return false;
6367
+ }
6368
+ function presentedToken(req) {
6369
+ const auth = req.headers.authorization;
6370
+ if (typeof auth === "string") {
6371
+ const m = auth.match(/^Bearer\s+(.+)$/i);
6372
+ if (m) return m[1].trim();
6373
+ }
6374
+ const q = req.query.token;
6375
+ return typeof q === "string" ? q : "";
6376
+ }
6377
+ function sameToken(a, b) {
6378
+ const ab = Buffer.from(a);
6379
+ const bb = Buffer.from(b);
6380
+ return ab.length === bb.length && timingSafeEqual(ab, bb);
6381
+ }
6382
+ function agentAccess(token, opensSession) {
6383
+ return (req, res, next) => {
6384
+ if (!token) {
6385
+ if (!isLocalHost(req.headers.host)) {
6386
+ res.status(403).json({ error: "Forbidden host" });
6387
+ return;
6388
+ }
6389
+ next();
6390
+ return;
6391
+ }
6392
+ if (opensSession(req) && !sameToken(presentedToken(req), token)) {
6393
+ res.status(401).json({ error: "Missing or wrong gateway token" });
6394
+ return;
6395
+ }
6396
+ next();
6397
+ };
6398
+ }
6399
+
6278
6400
  // src/lib/profile-loader.ts
6279
6401
  import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
6280
6402
  import { join as join15, resolve as resolve2 } from "path";
@@ -7008,6 +7130,13 @@ var dataDir = process.env.SUVEREN_DATA_DIR ?? join19(homedir11(), ".suveren");
7008
7130
  var spUrl = resolveAsUrl(dataDir);
7009
7131
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
7010
7132
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
7133
+ var bindHost = resolveBindHost();
7134
+ var agentToken = resolveAgentToken();
7135
+ var bindProblem = bindRefusal();
7136
+ if (bindProblem) {
7137
+ console.error(`[Suveren MCP] Refusing to start: ${bindProblem}`);
7138
+ process.exit(1);
7139
+ }
7011
7140
  setAsBaseUrl(spUrl);
7012
7141
  var state = new SharedState(spUrl, void 0, dataDir);
7013
7142
  var reportSources = {
@@ -7051,16 +7180,6 @@ integrationManager.setOnConfigMigrated((id, updates) => {
7051
7180
  });
7052
7181
  var app = express();
7053
7182
  app.use(express.json());
7054
- app.use((_req, res, next) => {
7055
- res.header("Access-Control-Allow-Origin", "http://localhost:3000");
7056
- res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
7057
- res.header("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
7058
- if (_req.method === "OPTIONS") {
7059
- res.sendStatus(204);
7060
- return;
7061
- }
7062
- next();
7063
- });
7064
7183
  var INTERNAL_SECRET = process.env.SUVEREN_INTERNAL_SECRET ?? "";
7065
7184
  function internalOnly(req, res, next) {
7066
7185
  const ip = req.ip ?? req.socket.remoteAddress ?? "";
@@ -7296,7 +7415,11 @@ app.post("/internal/stop-all-running", internalOnly, async (_req, res) => {
7296
7415
  }
7297
7416
  });
7298
7417
  var sseSessions = /* @__PURE__ */ new Map();
7299
- app.get("/sse", async (_req, res) => {
7418
+ var guardAgent = agentAccess(
7419
+ agentToken,
7420
+ (req) => req.path === "/sse" && req.method === "GET" || req.path === "/mcp" && req.method === "POST" && !req.headers["mcp-session-id"]
7421
+ );
7422
+ app.get("/sse", guardAgent, async (_req, res) => {
7300
7423
  const transport = new SSEServerTransport("/messages", res);
7301
7424
  const { server, refreshTools, registerProxiedTools } = createMcpServer(state, integrationManager);
7302
7425
  const sessionId = transport.sessionId;
@@ -7308,12 +7431,9 @@ app.get("/sse", async (_req, res) => {
7308
7431
  activeSessions.delete(sessionId);
7309
7432
  console.error(`[Suveren MCP] SSE session ${sessionId} closed`);
7310
7433
  });
7311
- server.registerTool("debug_test_tool", { description: "Debug test" }, async () => ({
7312
- content: [{ type: "text", text: "debug" }]
7313
- }));
7314
7434
  await server.connect(transport);
7315
7435
  });
7316
- app.post("/messages", async (req, res) => {
7436
+ app.post("/messages", guardAgent, async (req, res) => {
7317
7437
  const sessionId = req.query.sessionId;
7318
7438
  const transport = sseSessions.get(sessionId);
7319
7439
  if (!transport) {
@@ -7323,7 +7443,7 @@ app.post("/messages", async (req, res) => {
7323
7443
  await transport.handlePostMessage(req, res, req.body);
7324
7444
  });
7325
7445
  var streamableSessions = /* @__PURE__ */ new Map();
7326
- app.all("/mcp", async (req, res) => {
7446
+ app.all("/mcp", guardAgent, async (req, res) => {
7327
7447
  const sessionId = req.headers["mcp-session-id"];
7328
7448
  if (req.method === "GET" || req.method === "POST" || req.method === "DELETE") {
7329
7449
  if (sessionId && streamableSessions.has(sessionId)) {
@@ -7642,10 +7762,11 @@ process.on("SIGINT", async () => {
7642
7762
  await integrationManager.shutdown();
7643
7763
  process.exit(0);
7644
7764
  });
7645
- app.listen(port, "0.0.0.0", () => {
7646
- console.error(`[Suveren MCP] HTTP server listening on http://0.0.0.0:${port}`);
7647
- console.error(`[Suveren MCP] SSE: http://0.0.0.0:${port}/sse`);
7648
- console.error(`[Suveren MCP] Streamable: http://0.0.0.0:${port}/mcp`);
7765
+ app.listen(port, bindHost, () => {
7766
+ console.error(`[Suveren MCP] HTTP server listening on http://${bindHost}:${port}`);
7767
+ console.error(`[Suveren MCP] SSE: http://${bindHost}:${port}/sse`);
7768
+ console.error(`[Suveren MCP] Streamable: http://${bindHost}:${port}/mcp`);
7769
+ console.error(`[Suveren MCP] Agent token: ${agentToken ? "required" : "not set (this machine only)"}`);
7649
7770
  console.error(`[Suveren MCP] SP server: ${spUrl}`);
7650
7771
  profilesLoaded = loadProfiles();
7651
7772
  loadManifests();