@strixgov/mcp-proxy 0.2.0 → 0.2.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.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Config loader tests — the CLI's only durable input is the config
3
+ * file, so the loader's validation rules are the proxy's public
4
+ * contract for operator-managed deployments.
5
+ */
6
+
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ import { loadConfig, resolveCapabilities } from "../src/config.mjs";
14
+
15
+ async function withTempConfig(contents, fn) {
16
+ const dir = mkdtempSync(path.join(tmpdir(), "strix-proxy-config-"));
17
+ const file = path.join(dir, "proxy.json");
18
+ writeFileSync(file, typeof contents === "string" ? contents : JSON.stringify(contents));
19
+ try { return await fn(file); } finally { rmSync(dir, { recursive: true, force: true }); }
20
+ }
21
+
22
+ test("loadConfig: rejects missing file", async () => {
23
+ await assert.rejects(loadConfig("/this/path/does/not/exist.json"), /could not read/);
24
+ });
25
+
26
+ test("loadConfig: rejects malformed JSON", async () => {
27
+ await withTempConfig("{ not: json }", async (file) => {
28
+ await assert.rejects(loadConfig(file), /not valid JSON/);
29
+ });
30
+ });
31
+
32
+ test("loadConfig: rejects non-object root", async () => {
33
+ await withTempConfig("[]", async (file) => {
34
+ await assert.rejects(loadConfig(file), /must be a JSON object/);
35
+ });
36
+ });
37
+
38
+ test("loadConfig: requires upstream.command", async () => {
39
+ await withTempConfig({ upstream: {} }, async (file) => {
40
+ await assert.rejects(loadConfig(file), /upstream\.command/);
41
+ });
42
+ });
43
+
44
+ test("loadConfig: rejects non-array upstream.args", async () => {
45
+ await withTempConfig({ upstream: { command: "npx", args: "oops" } }, async (file) => {
46
+ await assert.rejects(loadConfig(file), /upstream\.args/);
47
+ });
48
+ });
49
+
50
+ test("loadConfig: rejects non-object policy", async () => {
51
+ await withTempConfig({ upstream: { command: "x" }, policy: "deny-all" }, async (file) => {
52
+ await assert.rejects(loadConfig(file), /'policy' must be an object/);
53
+ });
54
+ });
55
+
56
+ test("loadConfig: expands a leading ~ in storagePath", async () => {
57
+ await withTempConfig(
58
+ { upstream: { command: "npx" }, storagePath: "~/strix-receipts" },
59
+ async (file) => {
60
+ const cfg = await loadConfig(file);
61
+ assert.ok(!cfg.storagePath.startsWith("~"), `expected ~ to expand; got ${cfg.storagePath}`);
62
+ },
63
+ );
64
+ });
65
+
66
+ test("loadConfig: returns the parsed config when valid", async () => {
67
+ await withTempConfig(
68
+ {
69
+ serverId: "notion",
70
+ upstream: { command: "npx", args: ["-y", "@notionhq/notion-mcp-server"] },
71
+ policy: { default: "DENY" },
72
+ },
73
+ async (file) => {
74
+ const cfg = await loadConfig(file);
75
+ assert.equal(cfg.serverId, "notion");
76
+ assert.equal(cfg.upstream.command, "npx");
77
+ assert.deepEqual(cfg.upstream.args, ["-y", "@notionhq/notion-mcp-server"]);
78
+ },
79
+ );
80
+ });
81
+
82
+ test("loadConfig: missing keyPath is fine (undefined passes through)", async () => {
83
+ await withTempConfig({ upstream: { command: "npx" } }, async (file) => {
84
+ const cfg = await loadConfig(file);
85
+ assert.equal(cfg.keyPath, undefined);
86
+ });
87
+ });
88
+
89
+ test("loadConfig: rejects non-string keyPath", async () => {
90
+ await withTempConfig({ upstream: { command: "npx" }, keyPath: 42 }, async (file) => {
91
+ await assert.rejects(loadConfig(file), /'keyPath' must be a string/);
92
+ });
93
+ });
94
+
95
+ test("loadConfig: expands a leading ~ in keyPath", async () => {
96
+ await withTempConfig(
97
+ { upstream: { command: "npx" }, keyPath: "~/.strix-mcp-proxy/my-server/keys" },
98
+ async (file) => {
99
+ const cfg = await loadConfig(file);
100
+ assert.ok(!cfg.keyPath.startsWith("~"), `expected ~ to expand; got ${cfg.keyPath}`);
101
+ assert.ok(cfg.keyPath.includes(".strix-mcp-proxy/my-server/keys"));
102
+ },
103
+ );
104
+ });
105
+
106
+ test("loadConfig: passes a valid string keyPath through unchanged", async () => {
107
+ await withTempConfig(
108
+ { upstream: { command: "npx" }, keyPath: "/opt/strix/keys" },
109
+ async (file) => {
110
+ const cfg = await loadConfig(file);
111
+ assert.equal(cfg.keyPath, "/opt/strix/keys");
112
+ },
113
+ );
114
+ });
115
+
116
+ test("resolveCapabilities: passes an inline array through unchanged", async () => {
117
+ const arr = [{ id: "x", name: "x", risk: "LOW", mode: "READ", description: "x" }];
118
+ const out = await resolveCapabilities(arr);
119
+ assert.equal(out, arr);
120
+ });
121
+
122
+ test("resolveCapabilities: returns undefined for null/undefined input", async () => {
123
+ assert.equal(await resolveCapabilities(undefined), undefined);
124
+ assert.equal(await resolveCapabilities(null), undefined);
125
+ });
126
+
127
+ test("resolveCapabilities: rejects non-string non-array input", async () => {
128
+ await assert.rejects(resolveCapabilities(42), /must be a string import path or an array/);
129
+ });
130
+
131
+ test("resolveCapabilities: resolves a companion-pack module by import path", async () => {
132
+ // Uses the workspace-symlinked package — exercises the real
133
+ // dynamic-import flow the CLI would use in production.
134
+ const caps = await resolveCapabilities("@strixgov/capabilities-mcp-common/notion");
135
+ assert.ok(Array.isArray(caps));
136
+ assert.ok(caps.length > 0);
137
+ assert.ok(caps.every((c) => c.id?.startsWith("mcp.notion.")));
138
+ });
@@ -0,0 +1,69 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ const exampleDir = path.resolve(here, "../examples/nvidia-nooa");
9
+
10
+ test("NOOA .mcp.json launches the Strix proxy over stdio with trusted identity", async () => {
11
+ const config = JSON.parse(
12
+ await fs.readFile(path.join(exampleDir, ".mcp.json"), "utf8"),
13
+ );
14
+ const server = config.mcpServers.strix_secure_coding;
15
+ assert.equal(server.transport, "stdio");
16
+ assert.match(server.args.join(" "), /strix-mcp-proxy\.mjs/);
17
+ assert.equal(server.env.STRIX_REQUIRE_TRUSTED_IDENTITY, "true");
18
+ assert.match(server.env.STRIX_TRUSTED_ACTOR_ID, /^spiffe:\/\//);
19
+ assert.equal(server.env.STRIX_TENANT_ID, "nvidia-nooa-demo");
20
+ assert.equal(server.env.STRIX_ENVIRONMENT, "openshell");
21
+ });
22
+
23
+ test("NOOA proxy policy is fail-closed and protects consequential actions", async () => {
24
+ const config = JSON.parse(
25
+ await fs.readFile(path.join(exampleDir, "proxy-config.json"), "utf8"),
26
+ );
27
+ assert.equal(config.policy.default, "DENY");
28
+ assert.equal(
29
+ config.policy.rules["mcp.nooa-secure-coding.inspect_repository"],
30
+ "ALLOW",
31
+ );
32
+ assert.equal(
33
+ config.policy.rules["mcp.nooa-secure-coding.propose_patch"],
34
+ "ALLOW",
35
+ );
36
+ assert.equal(
37
+ config.policy.rules["mcp.nooa-secure-coding.merge_production"],
38
+ "APPROVAL_REQUIRED",
39
+ );
40
+ assert.equal(
41
+ config.policy.rules["mcp.nooa-secure-coding.rotate_credentials"],
42
+ "DENY",
43
+ );
44
+ assert.equal(config.approval.type, "file");
45
+ assert.ok(config.storagePath);
46
+ assert.ok(config.keyPath);
47
+ });
48
+
49
+ test("OpenShell template leaves Python without ordinary downstream egress", async () => {
50
+ const policy = await fs.readFile(
51
+ path.join(exampleDir, "openshell-policy.yaml"),
52
+ "utf8",
53
+ );
54
+ assert.match(policy, /^version: 1/m);
55
+ assert.match(policy, /run_as_user: sandbox/);
56
+ assert.match(policy, /api\.github\.com/);
57
+ assert.match(policy, /protocol: rest/);
58
+ assert.match(policy, /tls: terminate/);
59
+ assert.match(policy, /enforcement: enforce/);
60
+ assert.match(policy, /access: read-only/);
61
+ assert.doesNotMatch(policy, /path: \/usr\/bin\/python/);
62
+ assert.doesNotMatch(policy, /path: \/usr\/local\/bin\/python/);
63
+ });
64
+
65
+ test("NOOA agent uses MCPManager.create_from_server", async () => {
66
+ const source = await fs.readFile(path.join(exampleDir, "nooa_agent.py"), "utf8");
67
+ assert.match(source, /MCPManager\.create_from_server/);
68
+ assert.match(source, /strix_secure_coding/);
69
+ });
@@ -0,0 +1,20 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
9
+
10
+ const covered = (rel) =>
11
+ pkg.files.some((f) => rel === f || (f.endsWith("/") && rel.startsWith(f)));
12
+
13
+ test("every script file the `test` script runs ships in the tarball", () => {
14
+ const refs = [...pkg.scripts.test.matchAll(/\b(scripts\/[\w./-]+\.mjs)\b/g)].map((m) => m[1]);
15
+ assert.ok(refs.length > 0, "test script should reference a script file");
16
+ for (const rel of refs) {
17
+ assert.ok(existsSync(path.join(root, rel)), `${rel} is missing from the repo`);
18
+ assert.ok(covered(rel), `${rel} is used by \`npm test\` but excluded by package.json "files"`);
19
+ }
20
+ });