@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.
- package/dist/agent/config/mcpClientManager.cjs +48 -9
- package/dist/agent/config/mcpClientManager.d.ts +12 -0
- package/dist/agent/config/mcpClientManager.js +48 -9
- package/dist/agent/tests/mcpClientManager.test.cjs +50 -0
- package/dist/agent/tests/mcpClientManager.test.js +50 -0
- package/dist/cli/commands/skill.cjs +12 -4
- package/dist/cli/commands/skill.js +12 -4
- package/dist/cli/config/jsonSchema.cjs +55 -0
- package/dist/cli/config/jsonSchema.d.ts +2 -0
- package/dist/cli/config/jsonSchema.js +18 -0
- package/dist/cli/config/loader.cjs +33 -1
- package/dist/cli/config/loader.js +33 -1
- package/dist/cli/config/schema.cjs +119 -2
- package/dist/cli/config/schema.d.ts +40 -0
- package/dist/cli/config/schema.js +119 -2
- package/dist/cli/core/agentInvoker.cjs +4 -1
- package/dist/cli/core/agentInvoker.d.ts +3 -0
- package/dist/cli/core/agentInvoker.js +4 -1
- package/dist/cli/services/skillRepository.cjs +138 -20
- package/dist/cli/services/skillRepository.d.ts +10 -2
- package/dist/cli/services/skillRepository.js +138 -20
- package/dist/cli/services/skillSecurityScanner.cjs +158 -0
- package/dist/cli/services/skillSecurityScanner.d.ts +28 -0
- package/dist/cli/services/skillSecurityScanner.js +121 -0
- package/dist/cli/services/skillService.cjs +44 -12
- package/dist/cli/services/skillService.d.ts +2 -0
- package/dist/cli/services/skillService.js +46 -14
- package/dist/cli/types/skill.d.ts +9 -0
- package/dist/gateway/server.cjs +5 -1
- package/dist/gateway/server.js +5 -1
- package/dist/gateway/types.d.ts +9 -0
- package/dist/tests/cli-config-loader.test.cjs +33 -1
- package/dist/tests/cli-config-loader.test.js +33 -1
- package/dist/tests/config-json-schema.test.cjs +25 -0
- package/dist/tests/config-json-schema.test.d.ts +1 -0
- package/dist/tests/config-json-schema.test.js +19 -0
- package/dist/tests/skill-repository.test.cjs +106 -0
- package/dist/tests/skill-repository.test.d.ts +1 -0
- package/dist/tests/skill-repository.test.js +100 -0
- package/dist/tests/skill-security-scanner.test.cjs +126 -0
- package/dist/tests/skill-security-scanner.test.d.ts +1 -0
- package/dist/tests/skill-security-scanner.test.js +120 -0
- package/dist/tests/uv.test.cjs +47 -0
- package/dist/tests/uv.test.d.ts +1 -0
- package/dist/tests/uv.test.js +41 -0
- package/dist/utils/uv.cjs +64 -0
- package/dist/utils/uv.d.ts +3 -0
- package/dist/utils/uv.js +24 -0
- package/package.json +2 -1
- package/skills/gog/SKILL.md +36 -0
- 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
|
-
|
|
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.
|
|
117
|
-
|
|
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 =
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
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 =
|
|
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(
|
|
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
|
}
|
package/dist/gateway/server.cjs
CHANGED
|
@@ -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, {
|
package/dist/gateway/server.js
CHANGED
|
@@ -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, {
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
});
|