@wingman-ai/gateway 0.3.2 → 0.4.0

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.
Files changed (51) hide show
  1. package/dist/agent/config/mcpClientManager.cjs +48 -9
  2. package/dist/agent/config/mcpClientManager.d.ts +12 -0
  3. package/dist/agent/config/mcpClientManager.js +48 -9
  4. package/dist/agent/tests/mcpClientManager.test.cjs +50 -0
  5. package/dist/agent/tests/mcpClientManager.test.js +50 -0
  6. package/dist/cli/commands/skill.cjs +12 -4
  7. package/dist/cli/commands/skill.js +12 -4
  8. package/dist/cli/config/jsonSchema.cjs +55 -0
  9. package/dist/cli/config/jsonSchema.d.ts +2 -0
  10. package/dist/cli/config/jsonSchema.js +18 -0
  11. package/dist/cli/config/loader.cjs +33 -1
  12. package/dist/cli/config/loader.js +33 -1
  13. package/dist/cli/config/schema.cjs +119 -2
  14. package/dist/cli/config/schema.d.ts +40 -0
  15. package/dist/cli/config/schema.js +119 -2
  16. package/dist/cli/core/agentInvoker.cjs +4 -1
  17. package/dist/cli/core/agentInvoker.d.ts +3 -0
  18. package/dist/cli/core/agentInvoker.js +4 -1
  19. package/dist/cli/services/skillRepository.cjs +138 -20
  20. package/dist/cli/services/skillRepository.d.ts +10 -2
  21. package/dist/cli/services/skillRepository.js +138 -20
  22. package/dist/cli/services/skillSecurityScanner.cjs +158 -0
  23. package/dist/cli/services/skillSecurityScanner.d.ts +28 -0
  24. package/dist/cli/services/skillSecurityScanner.js +121 -0
  25. package/dist/cli/services/skillService.cjs +44 -12
  26. package/dist/cli/services/skillService.d.ts +2 -0
  27. package/dist/cli/services/skillService.js +46 -14
  28. package/dist/cli/types/skill.d.ts +9 -0
  29. package/dist/gateway/server.cjs +5 -1
  30. package/dist/gateway/server.js +5 -1
  31. package/dist/gateway/types.d.ts +9 -0
  32. package/dist/tests/cli-config-loader.test.cjs +33 -1
  33. package/dist/tests/cli-config-loader.test.js +33 -1
  34. package/dist/tests/config-json-schema.test.cjs +25 -0
  35. package/dist/tests/config-json-schema.test.d.ts +1 -0
  36. package/dist/tests/config-json-schema.test.js +19 -0
  37. package/dist/tests/skill-repository.test.cjs +106 -0
  38. package/dist/tests/skill-repository.test.d.ts +1 -0
  39. package/dist/tests/skill-repository.test.js +100 -0
  40. package/dist/tests/skill-security-scanner.test.cjs +126 -0
  41. package/dist/tests/skill-security-scanner.test.d.ts +1 -0
  42. package/dist/tests/skill-security-scanner.test.js +120 -0
  43. package/dist/tests/uv.test.cjs +47 -0
  44. package/dist/tests/uv.test.d.ts +1 -0
  45. package/dist/tests/uv.test.js +41 -0
  46. package/dist/utils/uv.cjs +64 -0
  47. package/dist/utils/uv.d.ts +3 -0
  48. package/dist/utils/uv.js +24 -0
  49. package/package.json +2 -1
  50. package/skills/gog/SKILL.md +36 -0
  51. package/skills/weather/SKILL.md +49 -0
@@ -0,0 +1,121 @@
1
+ import { spawn } from "node:child_process";
2
+ import { ensureUvAvailableForFeature } from "../../utils/uv.js";
3
+ const DEFAULT_SCANNER_COMMAND = "uvx";
4
+ const DEFAULT_SCANNER_ARGS = [
5
+ "--from",
6
+ "mcp-scan>=0.4,<0.5",
7
+ "mcp-scan",
8
+ "--json",
9
+ "--skills"
10
+ ];
11
+ const DEFAULT_BLOCKED_CODES = [
12
+ "MCP501",
13
+ "MCP506",
14
+ "MCP507",
15
+ "MCP508",
16
+ "MCP509",
17
+ "MCP510",
18
+ "MCP511"
19
+ ];
20
+ function getScannerCommand(security) {
21
+ return security?.scannerCommand?.trim() || DEFAULT_SCANNER_COMMAND;
22
+ }
23
+ function getScannerArgs(security) {
24
+ if (Array.isArray(security?.scannerArgs) && security.scannerArgs.length > 0) return security.scannerArgs;
25
+ return DEFAULT_SCANNER_ARGS;
26
+ }
27
+ function getBlockedIssueCodes(security) {
28
+ const configured = security?.blockIssueCodes || DEFAULT_BLOCKED_CODES;
29
+ return new Set(configured.map((code)=>code.trim().toUpperCase()).filter(Boolean));
30
+ }
31
+ function parseScanResult(stdout) {
32
+ try {
33
+ return JSON.parse(stdout);
34
+ } catch {
35
+ const firstBrace = stdout.indexOf("{");
36
+ const lastBrace = stdout.lastIndexOf("}");
37
+ if (-1 === firstBrace || -1 === lastBrace || lastBrace < firstBrace) throw new Error("Scanner output did not include JSON");
38
+ const jsonPayload = stdout.slice(firstBrace, lastBrace + 1);
39
+ return JSON.parse(jsonPayload);
40
+ }
41
+ }
42
+ async function runCommand(command, args) {
43
+ return await new Promise((resolve, reject)=>{
44
+ const child = spawn(command, args, {
45
+ stdio: [
46
+ "ignore",
47
+ "pipe",
48
+ "pipe"
49
+ ]
50
+ });
51
+ let stdout = "";
52
+ let stderr = "";
53
+ child.stdout.on("data", (chunk)=>{
54
+ stdout += chunk.toString();
55
+ });
56
+ child.stderr.on("data", (chunk)=>{
57
+ stderr += chunk.toString();
58
+ });
59
+ child.on("error", reject);
60
+ child.on("close", (exitCode)=>{
61
+ resolve({
62
+ exitCode,
63
+ stdout,
64
+ stderr
65
+ });
66
+ });
67
+ });
68
+ }
69
+ async function scanSkillDirectory(skillPath, logger, security) {
70
+ const scanOnInstall = security?.scanOnInstall ?? true;
71
+ if (!scanOnInstall) return;
72
+ const command = getScannerCommand(security);
73
+ ensureUvAvailableForFeature(command, "skills.security.scanOnInstall");
74
+ const args = [
75
+ ...getScannerArgs(security),
76
+ skillPath
77
+ ];
78
+ logger.info(`Running skill security scan: ${command} ${args.join(" ")}`);
79
+ const result = await runCommand(command, args);
80
+ if (0 !== result.exitCode) {
81
+ const details = result.stderr.trim() || result.stdout.trim();
82
+ throw new Error(`Skill security scan failed with exit code ${result.exitCode ?? "unknown"}${details ? `: ${details}` : ""}`);
83
+ }
84
+ const parsed = parseScanResult(result.stdout);
85
+ const failedPaths = Object.entries(parsed).filter(([, value])=>Boolean(value.error && false !== value.error.is_failure));
86
+ if (failedPaths.length > 0) {
87
+ const formatted = failedPaths.map(([path, value])=>{
88
+ const category = value.error?.category ? ` (${value.error.category})` : "";
89
+ return `${path}: ${value.error?.message || "unknown scan error"}${category}`;
90
+ }).join("; ");
91
+ throw new Error(`Skill security scan reported errors: ${formatted}`);
92
+ }
93
+ const blockedCodes = getBlockedIssueCodes(security);
94
+ const blockingIssues = [];
95
+ const nonBlockingIssues = [];
96
+ for (const value of Object.values(parsed))for (const issue of value.issues || []){
97
+ const code = (issue.code || "").trim().toUpperCase();
98
+ if (!code) continue;
99
+ const issueDetails = {
100
+ code,
101
+ message: issue.message || ""
102
+ };
103
+ if (blockedCodes.has(code)) blockingIssues.push(issueDetails);
104
+ else nonBlockingIssues.push(issueDetails);
105
+ }
106
+ if (nonBlockingIssues.length > 0) {
107
+ const codes = Array.from(new Set(nonBlockingIssues.map((issue)=>issue.code)));
108
+ logger.warn(`Skill security scan returned non-blocking issues: ${codes.join(", ")}`);
109
+ }
110
+ if (blockingIssues.length > 0) {
111
+ const codes = Array.from(new Set(blockingIssues.map((issue)=>issue.code)));
112
+ throw new Error(`Skill security scan blocked installation due to issue codes: ${codes.join(", ")}`);
113
+ }
114
+ }
115
+ const __skillSecurityScanner = {
116
+ parseScanResult,
117
+ getScannerArgs,
118
+ getScannerCommand,
119
+ getBlockedIssueCodes
120
+ };
121
+ export { __skillSecurityScanner, scanSkillDirectory };
@@ -27,9 +27,11 @@ __webpack_require__.d(__webpack_exports__, {
27
27
  SkillService: ()=>SkillService
28
28
  });
29
29
  const promises_namespaceObject = require("node:fs/promises");
30
+ const external_node_os_namespaceObject = require("node:os");
30
31
  const external_node_path_namespaceObject = require("node:path");
31
32
  const external_node_readline_promises_namespaceObject = require("node:readline/promises");
32
33
  const external_logger_cjs_namespaceObject = require("../../logger.cjs");
34
+ const external_skillSecurityScanner_cjs_namespaceObject = require("./skillSecurityScanner.cjs");
33
35
  function _define_property(obj, key, value) {
34
36
  if (key in obj) Object.defineProperty(obj, key, {
35
37
  value: value,
@@ -85,6 +87,8 @@ class SkillService {
85
87
  }
86
88
  }
87
89
  async installSkill(skillName) {
90
+ let stagingRoot = null;
91
+ let shouldReplaceExisting = false;
88
92
  try {
89
93
  const nameRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
90
94
  if (!nameRegex.test(skillName)) throw new Error(`Invalid skill name '${skillName}': must be lowercase alphanumeric with hyphens only`);
@@ -94,11 +98,7 @@ class SkillService {
94
98
  if (exists) if ("interactive" === this.outputManager.getMode()) {
95
99
  const shouldOverwrite = await this.promptForOverwrite(skillName);
96
100
  if (!shouldOverwrite) return void console.log("\nInstallation cancelled.");
97
- this.logger.info("Removing existing skill...");
98
- await promises_namespaceObject.rm(skillPath, {
99
- recursive: true,
100
- force: true
101
- });
101
+ shouldReplaceExisting = true;
102
102
  } else throw new Error(`Skill '${skillName}' is already installed.`);
103
103
  this.logger.info("Fetching skill metadata...");
104
104
  const metadata = await this.repository.getSkillMetadata(skillName);
@@ -113,22 +113,39 @@ class SkillService {
113
113
  });
114
114
  this.logger.info("Downloading skill files...");
115
115
  const files = await this.repository.downloadSkill(skillName);
116
- await promises_namespaceObject.mkdir(this.getSkillsPath(), {
117
- recursive: true
118
- });
119
- await promises_namespaceObject.mkdir(skillPath, {
116
+ stagingRoot = await promises_namespaceObject.mkdtemp(external_node_path_namespaceObject.join((0, external_node_os_namespaceObject.tmpdir)(), "wingman-skill-"));
117
+ const stagedSkillPath = external_node_path_namespaceObject.join(stagingRoot, skillName);
118
+ await promises_namespaceObject.mkdir(stagedSkillPath, {
120
119
  recursive: true
121
120
  });
122
- this.logger.info(`Writing ${files.size} files...`);
121
+ this.logger.info(`Writing ${files.size} files to staging...`);
123
122
  for (const [relativePath, content] of files){
124
- const filePath = external_node_path_namespaceObject.join(skillPath, relativePath);
123
+ const filePath = this.resolveSafeInstallPath(stagedSkillPath, relativePath);
125
124
  const fileDir = external_node_path_namespaceObject.dirname(filePath);
126
125
  await promises_namespaceObject.mkdir(fileDir, {
127
126
  recursive: true
128
127
  });
129
128
  await promises_namespaceObject.writeFile(filePath, content);
130
129
  }
131
- await this.validateSkillMd(skillPath);
130
+ await this.validateSkillMd(stagedSkillPath);
131
+ await (0, external_skillSecurityScanner_cjs_namespaceObject.scanSkillDirectory)(stagedSkillPath, this.logger, this.security);
132
+ await promises_namespaceObject.mkdir(this.getSkillsPath(), {
133
+ recursive: true
134
+ });
135
+ if (shouldReplaceExisting) {
136
+ this.logger.info("Replacing existing skill...");
137
+ await promises_namespaceObject.rm(skillPath, {
138
+ recursive: true,
139
+ force: true
140
+ });
141
+ }
142
+ await promises_namespaceObject.mkdir(skillPath, {
143
+ recursive: true
144
+ });
145
+ await promises_namespaceObject.cp(stagedSkillPath, skillPath, {
146
+ recursive: true,
147
+ force: true
148
+ });
132
149
  if ("interactive" === this.outputManager.getMode()) console.log(`\n✓ Successfully installed skill ${skillName} to ${skillPath}`);
133
150
  else this.outputManager.emitEvent({
134
151
  type: "skill-install-complete",
@@ -150,6 +167,11 @@ class SkillService {
150
167
  timestamp: new Date().toISOString()
151
168
  });
152
169
  throw error;
170
+ } finally{
171
+ if (stagingRoot) await promises_namespaceObject.rm(stagingRoot, {
172
+ recursive: true,
173
+ force: true
174
+ });
153
175
  }
154
176
  }
155
177
  async listInstalledSkills() {
@@ -291,6 +313,14 @@ class SkillService {
291
313
  throw new Error(`Invalid SKILL.md: ${error instanceof Error ? error.message : String(error)}`);
292
314
  }
293
315
  }
316
+ resolveSafeInstallPath(root, relativePath) {
317
+ const normalized = external_node_path_namespaceObject.posix.normalize(relativePath.replace(/\\/g, "/")).replace(/^\/+/, "");
318
+ if (!normalized || "." === normalized || normalized.startsWith("../")) throw new Error(`Unsafe skill file path '${relativePath}' rejected during installation`);
319
+ const rootResolved = external_node_path_namespaceObject.resolve(root);
320
+ const filePath = external_node_path_namespaceObject.resolve(rootResolved, normalized);
321
+ if (filePath !== rootResolved && !filePath.startsWith(rootResolved + external_node_path_namespaceObject.sep)) throw new Error(`Unsafe skill file path '${relativePath}' rejected during installation`);
322
+ return filePath;
323
+ }
294
324
  parseSkillMetadata(content) {
295
325
  const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
296
326
  const match = content.match(frontmatterRegex);
@@ -320,11 +350,13 @@ class SkillService {
320
350
  _define_property(this, "repository", void 0);
321
351
  _define_property(this, "outputManager", void 0);
322
352
  _define_property(this, "logger", void 0);
353
+ _define_property(this, "security", void 0);
323
354
  this.repository = repository;
324
355
  this.outputManager = outputManager;
325
356
  this.logger = logger;
326
357
  this.workspace = options.workspace;
327
358
  this.skillsDirectory = options.skillsDirectory || "skills";
359
+ this.security = options.security || {};
328
360
  }
329
361
  }
330
362
  exports.SkillService = __webpack_exports__.SkillService;
@@ -8,6 +8,7 @@ export declare class SkillService {
8
8
  private readonly repository;
9
9
  private readonly outputManager;
10
10
  private readonly logger;
11
+ private readonly security;
11
12
  constructor(repository: SkillRepository, outputManager: OutputManager, logger: Logger, options: SkillServiceOptions);
12
13
  /**
13
14
  * Get the absolute path to the skills directory
@@ -41,6 +42,7 @@ export declare class SkillService {
41
42
  * Validate SKILL.md file
42
43
  */
43
44
  private validateSkillMd;
45
+ private resolveSafeInstallPath;
44
46
  /**
45
47
  * Parse SKILL.md metadata (same logic as repository)
46
48
  */
@@ -1,7 +1,9 @@
1
- import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
1
+ import { access, cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, join, posix, resolve, sep } from "node:path";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { getLogFilePath } from "../../logger.js";
6
+ import { scanSkillDirectory } from "./skillSecurityScanner.js";
5
7
  function _define_property(obj, key, value) {
6
8
  if (key in obj) Object.defineProperty(obj, key, {
7
9
  value: value,
@@ -57,6 +59,8 @@ class SkillService {
57
59
  }
58
60
  }
59
61
  async installSkill(skillName) {
62
+ let stagingRoot = null;
63
+ let shouldReplaceExisting = false;
60
64
  try {
61
65
  const nameRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
62
66
  if (!nameRegex.test(skillName)) throw new Error(`Invalid skill name '${skillName}': must be lowercase alphanumeric with hyphens only`);
@@ -66,11 +70,7 @@ class SkillService {
66
70
  if (exists) if ("interactive" === this.outputManager.getMode()) {
67
71
  const shouldOverwrite = await this.promptForOverwrite(skillName);
68
72
  if (!shouldOverwrite) return void console.log("\nInstallation cancelled.");
69
- this.logger.info("Removing existing skill...");
70
- await rm(skillPath, {
71
- recursive: true,
72
- force: true
73
- });
73
+ shouldReplaceExisting = true;
74
74
  } else throw new Error(`Skill '${skillName}' is already installed.`);
75
75
  this.logger.info("Fetching skill metadata...");
76
76
  const metadata = await this.repository.getSkillMetadata(skillName);
@@ -85,22 +85,39 @@ class SkillService {
85
85
  });
86
86
  this.logger.info("Downloading skill files...");
87
87
  const files = await this.repository.downloadSkill(skillName);
88
- await mkdir(this.getSkillsPath(), {
89
- recursive: true
90
- });
91
- await mkdir(skillPath, {
88
+ stagingRoot = await mkdtemp(join(tmpdir(), "wingman-skill-"));
89
+ const stagedSkillPath = join(stagingRoot, skillName);
90
+ await mkdir(stagedSkillPath, {
92
91
  recursive: true
93
92
  });
94
- this.logger.info(`Writing ${files.size} files...`);
93
+ this.logger.info(`Writing ${files.size} files to staging...`);
95
94
  for (const [relativePath, content] of files){
96
- const filePath = join(skillPath, relativePath);
95
+ const filePath = this.resolveSafeInstallPath(stagedSkillPath, relativePath);
97
96
  const fileDir = dirname(filePath);
98
97
  await mkdir(fileDir, {
99
98
  recursive: true
100
99
  });
101
100
  await writeFile(filePath, content);
102
101
  }
103
- await this.validateSkillMd(skillPath);
102
+ await this.validateSkillMd(stagedSkillPath);
103
+ await scanSkillDirectory(stagedSkillPath, this.logger, this.security);
104
+ await mkdir(this.getSkillsPath(), {
105
+ recursive: true
106
+ });
107
+ if (shouldReplaceExisting) {
108
+ this.logger.info("Replacing existing skill...");
109
+ await rm(skillPath, {
110
+ recursive: true,
111
+ force: true
112
+ });
113
+ }
114
+ await mkdir(skillPath, {
115
+ recursive: true
116
+ });
117
+ await cp(stagedSkillPath, skillPath, {
118
+ recursive: true,
119
+ force: true
120
+ });
104
121
  if ("interactive" === this.outputManager.getMode()) console.log(`\n✓ Successfully installed skill ${skillName} to ${skillPath}`);
105
122
  else this.outputManager.emitEvent({
106
123
  type: "skill-install-complete",
@@ -122,6 +139,11 @@ class SkillService {
122
139
  timestamp: new Date().toISOString()
123
140
  });
124
141
  throw error;
142
+ } finally{
143
+ if (stagingRoot) await rm(stagingRoot, {
144
+ recursive: true,
145
+ force: true
146
+ });
125
147
  }
126
148
  }
127
149
  async listInstalledSkills() {
@@ -263,6 +285,14 @@ class SkillService {
263
285
  throw new Error(`Invalid SKILL.md: ${error instanceof Error ? error.message : String(error)}`);
264
286
  }
265
287
  }
288
+ resolveSafeInstallPath(root, relativePath) {
289
+ const normalized = posix.normalize(relativePath.replace(/\\/g, "/")).replace(/^\/+/, "");
290
+ if (!normalized || "." === normalized || normalized.startsWith("../")) throw new Error(`Unsafe skill file path '${relativePath}' rejected during installation`);
291
+ const rootResolved = resolve(root);
292
+ const filePath = resolve(rootResolved, normalized);
293
+ if (filePath !== rootResolved && !filePath.startsWith(rootResolved + sep)) throw new Error(`Unsafe skill file path '${relativePath}' rejected during installation`);
294
+ return filePath;
295
+ }
266
296
  parseSkillMetadata(content) {
267
297
  const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
268
298
  const match = content.match(frontmatterRegex);
@@ -292,11 +322,13 @@ class SkillService {
292
322
  _define_property(this, "repository", void 0);
293
323
  _define_property(this, "outputManager", void 0);
294
324
  _define_property(this, "logger", void 0);
325
+ _define_property(this, "security", void 0);
295
326
  this.repository = repository;
296
327
  this.outputManager = outputManager;
297
328
  this.logger = logger;
298
329
  this.workspace = options.workspace;
299
330
  this.skillsDirectory = options.skillsDirectory || "skills";
331
+ this.security = options.security || {};
300
332
  }
301
333
  }
302
334
  export { SkillService };
@@ -57,9 +57,17 @@ export interface SkillCommandArgs {
57
57
  * Options for skill repository operations
58
58
  */
59
59
  export interface SkillRepositoryOptions {
60
+ provider?: "github" | "clawhub";
60
61
  repositoryOwner?: string;
61
62
  repositoryName?: string;
62
63
  githubToken?: string;
64
+ clawhubBaseUrl?: string;
65
+ }
66
+ export interface SkillSecurityOptions {
67
+ scanOnInstall?: boolean;
68
+ scannerCommand?: string;
69
+ scannerArgs?: string[];
70
+ blockIssueCodes?: string[];
63
71
  }
64
72
  /**
65
73
  * Options for skill service operations
@@ -68,4 +76,5 @@ export interface SkillServiceOptions {
68
76
  workspace: string;
69
77
  skillsDirectory?: string;
70
78
  outputMode: OutputMode;
79
+ security?: SkillSecurityOptions;
71
80
  }
@@ -41,6 +41,7 @@ const agentInvoker_cjs_namespaceObject = require("../cli/core/agentInvoker.cjs")
41
41
  const outputManager_cjs_namespaceObject = require("../cli/core/outputManager.cjs");
42
42
  const sessionManager_cjs_namespaceObject = require("../cli/core/sessionManager.cjs");
43
43
  const external_logger_cjs_namespaceObject = require("../logger.cjs");
44
+ const uv_cjs_namespaceObject = require("../utils/uv.cjs");
44
45
  const discord_cjs_namespaceObject = require("./adapters/discord.cjs");
45
46
  const external_auth_cjs_namespaceObject = require("./auth.cjs");
46
47
  const external_browserRelayServer_cjs_namespaceObject = require("./browserRelayServer.cjs");
@@ -99,6 +100,8 @@ function resolveExecutionConfigDirOverride(payload) {
99
100
  class GatewayServer {
100
101
  async start() {
101
102
  if (void 0 === globalThis.Bun) throw new Error("Gateway server requires Bun runtime. Start with `bun ./bin/wingman gateway start`.");
103
+ const proxyConfig = this.wingmanConfig.gateway?.mcpProxy;
104
+ if (proxyConfig?.enabled) (0, uv_cjs_namespaceObject.ensureUvAvailableForFeature)(proxyConfig.command || "uvx", "gateway.mcpProxy.enabled");
102
105
  this.startedAt = Date.now();
103
106
  this.internalHooks = new registry_cjs_namespaceObject.InternalHookRegistry(this.getHttpContext(), this.wingmanConfig.hooks);
104
107
  await this.internalHooks.load();
@@ -546,7 +549,8 @@ class GatewayServer {
546
549
  sessionManager,
547
550
  terminalSessionManager: this.terminalSessionManager,
548
551
  workdir,
549
- defaultOutputDir
552
+ defaultOutputDir,
553
+ mcpProxyConfig: this.wingmanConfig.gateway?.mcpProxy
550
554
  });
551
555
  const abortController = new AbortController();
552
556
  this.activeAgentRequests.set(msg.id, {
@@ -8,6 +8,7 @@ import { AgentInvoker } from "../cli/core/agentInvoker.js";
8
8
  import { OutputManager } from "../cli/core/outputManager.js";
9
9
  import { SessionManager } from "../cli/core/sessionManager.js";
10
10
  import { createLogger } from "../logger.js";
11
+ import { ensureUvAvailableForFeature } from "../utils/uv.js";
11
12
  import { DiscordGatewayAdapter } from "./adapters/discord.js";
12
13
  import { GatewayAuth } from "./auth.js";
13
14
  import { BrowserRelayServer } from "./browserRelayServer.js";
@@ -66,6 +67,8 @@ function resolveExecutionConfigDirOverride(payload) {
66
67
  class GatewayServer {
67
68
  async start() {
68
69
  if (void 0 === globalThis.Bun) throw new Error("Gateway server requires Bun runtime. Start with `bun ./bin/wingman gateway start`.");
70
+ const proxyConfig = this.wingmanConfig.gateway?.mcpProxy;
71
+ if (proxyConfig?.enabled) ensureUvAvailableForFeature(proxyConfig.command || "uvx", "gateway.mcpProxy.enabled");
69
72
  this.startedAt = Date.now();
70
73
  this.internalHooks = new InternalHookRegistry(this.getHttpContext(), this.wingmanConfig.hooks);
71
74
  await this.internalHooks.load();
@@ -513,7 +516,8 @@ class GatewayServer {
513
516
  sessionManager,
514
517
  terminalSessionManager: this.terminalSessionManager,
515
518
  workdir,
516
- defaultOutputDir
519
+ defaultOutputDir,
520
+ mcpProxyConfig: this.wingmanConfig.gateway?.mcpProxy
517
521
  });
518
522
  const abortController = new AbortController();
519
523
  this.activeAgentRequests.set(msg.id, {
@@ -143,6 +143,15 @@ export interface GatewayConfig {
143
143
  method: "mdns" | "tailscale";
144
144
  name: string;
145
145
  };
146
+ mcpProxy?: {
147
+ enabled: boolean;
148
+ command?: string;
149
+ baseArgs?: string[];
150
+ projectName?: string;
151
+ pushExplorer?: boolean;
152
+ apiKey?: string;
153
+ apiUrl?: string;
154
+ };
146
155
  }
147
156
  export interface GatewayAuthConfig {
148
157
  mode: "token" | "password" | "none";
@@ -75,9 +75,31 @@ const external_os_namespaceObject = require("os");
75
75
  outputMode: "auto"
76
76
  },
77
77
  skills: {
78
+ provider: "github",
78
79
  repositoryOwner: "anthropics",
79
80
  repositoryName: "skills",
80
- skillsDirectory: "skills"
81
+ clawhubBaseUrl: "https://clawhub.ai",
82
+ skillsDirectory: "skills",
83
+ security: {
84
+ scanOnInstall: true,
85
+ scannerCommand: "uvx",
86
+ scannerArgs: [
87
+ "--from",
88
+ "mcp-scan>=0.4,<0.5",
89
+ "mcp-scan",
90
+ "--json",
91
+ "--skills"
92
+ ],
93
+ blockIssueCodes: [
94
+ "MCP501",
95
+ "MCP506",
96
+ "MCP507",
97
+ "MCP508",
98
+ "MCP509",
99
+ "MCP510",
100
+ "MCP511"
101
+ ]
102
+ }
81
103
  },
82
104
  browser: {
83
105
  profilesDir: ".wingman/browser-profiles",
@@ -109,6 +131,16 @@ const external_os_namespaceObject = require("os");
109
131
  allowInsecureAuth: false
110
132
  },
111
133
  dynamicUiEnabled: true,
134
+ mcpProxy: {
135
+ enabled: false,
136
+ command: "uvx",
137
+ baseArgs: [
138
+ "invariant-gateway@latest",
139
+ "mcp"
140
+ ],
141
+ projectName: "wingman-gateway",
142
+ pushExplorer: false
143
+ },
112
144
  adapters: {}
113
145
  },
114
146
  agents: {
@@ -73,9 +73,31 @@ describe("CLI Config Loader", ()=>{
73
73
  outputMode: "auto"
74
74
  },
75
75
  skills: {
76
+ provider: "github",
76
77
  repositoryOwner: "anthropics",
77
78
  repositoryName: "skills",
78
- skillsDirectory: "skills"
79
+ clawhubBaseUrl: "https://clawhub.ai",
80
+ skillsDirectory: "skills",
81
+ security: {
82
+ scanOnInstall: true,
83
+ scannerCommand: "uvx",
84
+ scannerArgs: [
85
+ "--from",
86
+ "mcp-scan>=0.4,<0.5",
87
+ "mcp-scan",
88
+ "--json",
89
+ "--skills"
90
+ ],
91
+ blockIssueCodes: [
92
+ "MCP501",
93
+ "MCP506",
94
+ "MCP507",
95
+ "MCP508",
96
+ "MCP509",
97
+ "MCP510",
98
+ "MCP511"
99
+ ]
100
+ }
79
101
  },
80
102
  browser: {
81
103
  profilesDir: ".wingman/browser-profiles",
@@ -107,6 +129,16 @@ describe("CLI Config Loader", ()=>{
107
129
  allowInsecureAuth: false
108
130
  },
109
131
  dynamicUiEnabled: true,
132
+ mcpProxy: {
133
+ enabled: false,
134
+ command: "uvx",
135
+ baseArgs: [
136
+ "invariant-gateway@latest",
137
+ "mcp"
138
+ ],
139
+ projectName: "wingman-gateway",
140
+ pushExplorer: false
141
+ },
110
142
  adapters: {}
111
143
  },
112
144
  agents: {
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __webpack_exports__ = {};
3
+ const external_vitest_namespaceObject = require("vitest");
4
+ const jsonSchema_cjs_namespaceObject = require("../cli/config/jsonSchema.cjs");
5
+ (0, external_vitest_namespaceObject.describe)("wingman config json schema", ()=>{
6
+ (0, external_vitest_namespaceObject.it)("includes metadata and top-level sections", ()=>{
7
+ const schema = (0, jsonSchema_cjs_namespaceObject.buildWingmanConfigJsonSchema)();
8
+ (0, external_vitest_namespaceObject.expect)(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
9
+ (0, external_vitest_namespaceObject.expect)(schema.$id).toBe(jsonSchema_cjs_namespaceObject.WINGMAN_CONFIG_JSON_SCHEMA_ID);
10
+ (0, external_vitest_namespaceObject.expect)(schema.title).toBe("Wingman Config");
11
+ (0, external_vitest_namespaceObject.expect)(schema.type).toBe("object");
12
+ (0, external_vitest_namespaceObject.expect)(schema.properties).toBeDefined();
13
+ (0, external_vitest_namespaceObject.expect)(schema.properties.gateway).toBeDefined();
14
+ (0, external_vitest_namespaceObject.expect)(schema.properties.skills).toBeDefined();
15
+ });
16
+ (0, external_vitest_namespaceObject.it)("exposes resilient default scanner args", ()=>{
17
+ const schema = (0, jsonSchema_cjs_namespaceObject.buildWingmanConfigJsonSchema)();
18
+ const scannerArgsDefault = schema.properties.skills.properties.security.properties.scannerArgs.default;
19
+ (0, external_vitest_namespaceObject.expect)(scannerArgsDefault).toContain("mcp-scan>=0.4,<0.5");
20
+ });
21
+ });
22
+ for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
23
+ Object.defineProperty(exports, '__esModule', {
24
+ value: true
25
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { WINGMAN_CONFIG_JSON_SCHEMA_ID, buildWingmanConfigJsonSchema } from "../cli/config/jsonSchema.js";
3
+ describe("wingman config json schema", ()=>{
4
+ it("includes metadata and top-level sections", ()=>{
5
+ const schema = buildWingmanConfigJsonSchema();
6
+ expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
7
+ expect(schema.$id).toBe(WINGMAN_CONFIG_JSON_SCHEMA_ID);
8
+ expect(schema.title).toBe("Wingman Config");
9
+ expect(schema.type).toBe("object");
10
+ expect(schema.properties).toBeDefined();
11
+ expect(schema.properties.gateway).toBeDefined();
12
+ expect(schema.properties.skills).toBeDefined();
13
+ });
14
+ it("exposes resilient default scanner args", ()=>{
15
+ const schema = buildWingmanConfigJsonSchema();
16
+ const scannerArgsDefault = schema.properties.skills.properties.security.properties.scannerArgs.default;
17
+ expect(scannerArgsDefault).toContain("mcp-scan>=0.4,<0.5");
18
+ });
19
+ });