@skaleagents/swarm 0.6.18 → 0.6.19
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/application/tools.js +10 -3
- package/dist/http.js +28 -25
- package/dist/saved-review.d.ts +36 -0
- package/dist/saved-review.js +177 -0
- package/dist/server.js +19 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,14 +3,16 @@ import { requireApiAuth, unauthorizedContent } from "../auth.js";
|
|
|
3
3
|
import { ScanInputError } from "../iac/parse.js";
|
|
4
4
|
import { contextSchema, fileSchema, planReview, consultationInstructions, } from "./intake.js";
|
|
5
5
|
import { reviewApplication } from "./review.js";
|
|
6
|
-
|
|
6
|
+
import { applicationTargetSchema, withSavedReview } from "../saved-review.js";
|
|
7
|
+
async function run(remote, compute, save) {
|
|
7
8
|
if (!remote) {
|
|
8
9
|
const auth = await requireApiAuth();
|
|
9
10
|
if (!auth.ok)
|
|
10
11
|
return unauthorizedContent(auth);
|
|
11
12
|
}
|
|
12
13
|
try {
|
|
13
|
-
const
|
|
14
|
+
const computed = compute();
|
|
15
|
+
const output = save ? await withSavedReview(computed, save) : computed;
|
|
14
16
|
return {
|
|
15
17
|
content: [
|
|
16
18
|
{ type: "text", text: JSON.stringify(output, null, 2) },
|
|
@@ -67,8 +69,13 @@ export function registerConsultation(server, remote) {
|
|
|
67
69
|
.max(80)
|
|
68
70
|
.describe("Related source/config/test files with repository-relative paths; include package.json and tsconfig.json. At most 500,000 combined content characters"),
|
|
69
71
|
context: contextSchema,
|
|
72
|
+
target: applicationTargetSchema,
|
|
70
73
|
},
|
|
71
|
-
}, ({ files, context }) => run(remote, () => reviewApplication(files, context)
|
|
74
|
+
}, ({ files, context, target }) => run(remote, () => reviewApplication(files, context), {
|
|
75
|
+
tool: "review_application_architecture",
|
|
76
|
+
target: target ?? null,
|
|
77
|
+
submittedPaths: files.map((file) => file.path),
|
|
78
|
+
}));
|
|
72
79
|
server.registerPrompt("architecture_consultation", {
|
|
73
80
|
title: "Independent application architecture consultation",
|
|
74
81
|
description: "Guide a multi-turn whole-application review with source evidence, follow-up questions, and design tradeoffs.",
|
package/dist/http.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
2
2
|
import { createServer } from "./server.js";
|
|
3
3
|
import { getApiUrl } from "./config.js";
|
|
4
|
+
import { requestAuthorization } from "./saved-review.js";
|
|
4
5
|
const resource = process.env.MCP_RESOURCE_URL ?? "https://skaleagents.com/mcp";
|
|
5
6
|
const metadataUrl = `${new URL(resource).origin}/.well-known/oauth-protected-resource/mcp`;
|
|
6
7
|
const allowedOrigins = new Set([
|
|
@@ -105,30 +106,32 @@ export async function handleMcpRequest(request) {
|
|
|
105
106
|
error: { code: -32700, message: "Invalid JSON" },
|
|
106
107
|
});
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await server.connect(transport);
|
|
115
|
-
const response = await transport.handleRequest(request, {
|
|
116
|
-
parsedBody: body,
|
|
109
|
+
return requestAuthorization.run(authorization, async () => {
|
|
110
|
+
const server = createServer(true);
|
|
111
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
112
|
+
sessionIdGenerator: undefined,
|
|
113
|
+
enableJsonResponse: true,
|
|
117
114
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
115
|
+
try {
|
|
116
|
+
await server.connect(transport);
|
|
117
|
+
const response = await transport.handleRequest(request, {
|
|
118
|
+
parsedBody: body,
|
|
119
|
+
});
|
|
120
|
+
// Finish the JSON response before closing this request-scoped transport.
|
|
121
|
+
const responseBody = await response.arrayBuffer();
|
|
122
|
+
const responseHeaders = new Headers(response.headers);
|
|
123
|
+
for (const [key, value] of Object.entries(headers))
|
|
124
|
+
responseHeaders.set(key, value);
|
|
125
|
+
return new Response(responseBody.byteLength ? responseBody : null, {
|
|
126
|
+
status: response.status,
|
|
127
|
+
headers: responseHeaders,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return json(500, { error: "mcp_request_failed" });
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await server.close();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
134
137
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export declare const savedTargetSchema: z.ZodOptional<z.ZodObject<{
|
|
4
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
5
|
+
path: z.ZodOptional<z.ZodString>;
|
|
6
|
+
revision: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, z.core.$strict>>;
|
|
8
|
+
export declare const applicationTargetSchema: z.ZodOptional<z.ZodObject<{
|
|
9
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
10
|
+
revision: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strict>>;
|
|
12
|
+
export declare const requestAuthorization: AsyncLocalStorage<string>;
|
|
13
|
+
export declare const ASSISTANT_INSTRUCTION = "Report the headline and the one recommended change. Include the share link. If the headline says the finding is gone, say that. If there are no findings, say that this does not prove the system is secure. Do not paste source, diffs, or account details. Do not invent a link when the share link is missing.";
|
|
14
|
+
type Target = {
|
|
15
|
+
repository?: string;
|
|
16
|
+
path?: string;
|
|
17
|
+
revision?: string;
|
|
18
|
+
};
|
|
19
|
+
type SaveInput = {
|
|
20
|
+
tool: string;
|
|
21
|
+
target?: Target | null;
|
|
22
|
+
submittedPaths: string[];
|
|
23
|
+
};
|
|
24
|
+
export declare function buildProjection(input: SaveInput, output: Record<string, unknown>): {
|
|
25
|
+
tool: string;
|
|
26
|
+
engineVersion: string;
|
|
27
|
+
target: Target | null;
|
|
28
|
+
riskSentence: unknown;
|
|
29
|
+
findings: Record<string, unknown>[];
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
checked: string[];
|
|
32
|
+
skipped: string[];
|
|
33
|
+
submittedPaths: string[];
|
|
34
|
+
};
|
|
35
|
+
export declare function withSavedReview(output: Record<string, unknown>, input: SaveInput): Promise<Record<string, unknown>>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getApiAccessToken } from "./auth.js";
|
|
4
|
+
import { getApiUrl } from "./config.js";
|
|
5
|
+
import { VERSION } from "./version.js";
|
|
6
|
+
const repositoryLabel = z
|
|
7
|
+
.string()
|
|
8
|
+
.regex(/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)?$/)
|
|
9
|
+
.max(200);
|
|
10
|
+
const relativePath = z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(500)
|
|
14
|
+
.refine((path) => !path.startsWith("/") && !path.includes("..") && !path.includes("\\"));
|
|
15
|
+
const revision = z.string().regex(/^[A-Za-z0-9._-]{1,64}$/);
|
|
16
|
+
export const savedTargetSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
repository: repositoryLabel.optional(),
|
|
19
|
+
path: relativePath.optional(),
|
|
20
|
+
revision: revision.optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Repository label for the next comparison. The server does not fetch it.");
|
|
25
|
+
export const applicationTargetSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
repository: repositoryLabel.optional(),
|
|
28
|
+
revision: revision.optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Repository label for the next comparison. Send paths on each file.");
|
|
33
|
+
export const requestAuthorization = new AsyncLocalStorage();
|
|
34
|
+
export const ASSISTANT_INSTRUCTION = "Report the headline and the one recommended change. Include the share link. If the headline says the finding is gone, say that. If there are no findings, say that this does not prove the system is secure. Do not paste source, diffs, or account details. Do not invent a link when the share link is missing.";
|
|
35
|
+
const refused = {
|
|
36
|
+
saved: false,
|
|
37
|
+
id: null,
|
|
38
|
+
ownerUrl: null,
|
|
39
|
+
shareUrl: null,
|
|
40
|
+
shareExpiresAt: null,
|
|
41
|
+
comparison: null,
|
|
42
|
+
headline: null,
|
|
43
|
+
assistantInstruction: ASSISTANT_INSTRUCTION,
|
|
44
|
+
};
|
|
45
|
+
function clip(value, max = 200) {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
const text = value.replaceAll("\n", " ").trim();
|
|
49
|
+
if (!text || text.length > max)
|
|
50
|
+
return null;
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
function findingPath(finding, fallback) {
|
|
54
|
+
const location = finding.location;
|
|
55
|
+
if (location && typeof location === "object" && "path" in location && fallback) {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
const evidence = finding.evidence;
|
|
59
|
+
if (Array.isArray(evidence) && evidence[0] && typeof evidence[0] === "object" && "file" in evidence[0]) {
|
|
60
|
+
const file = clip(evidence[0].file, 500);
|
|
61
|
+
if (file)
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
export function buildProjection(input, output) {
|
|
67
|
+
const findings = Array.isArray(output.findings) ? output.findings : [];
|
|
68
|
+
const fallbackPath = input.target?.path ?? "";
|
|
69
|
+
const projected = findings.slice(0, 100).flatMap((item) => {
|
|
70
|
+
if (!item || typeof item !== "object")
|
|
71
|
+
return [];
|
|
72
|
+
const finding = item;
|
|
73
|
+
const title = clip(finding.title);
|
|
74
|
+
const severity = clip(finding.severity, 16);
|
|
75
|
+
if (!title || !severity)
|
|
76
|
+
return [];
|
|
77
|
+
const row = { title, severity };
|
|
78
|
+
const ruleId = clip(finding.ruleId, 64);
|
|
79
|
+
if (ruleId)
|
|
80
|
+
row.ruleId = ruleId;
|
|
81
|
+
for (const field of ["detail", "resource", "remediation"]) {
|
|
82
|
+
const text = clip(finding[field]);
|
|
83
|
+
if (text)
|
|
84
|
+
row[field] = text;
|
|
85
|
+
}
|
|
86
|
+
const path = findingPath(finding, fallbackPath);
|
|
87
|
+
if (path)
|
|
88
|
+
row.path = path;
|
|
89
|
+
const line = finding.location && typeof finding.location === "object" && "line" in finding.location
|
|
90
|
+
? finding.location.line
|
|
91
|
+
: Array.isArray(finding.evidence) && finding.evidence[0] && typeof finding.evidence[0] === "object"
|
|
92
|
+
? finding.evidence[0].line
|
|
93
|
+
: null;
|
|
94
|
+
if (typeof line === "number" && Number.isInteger(line) && line > 0)
|
|
95
|
+
row.line = line;
|
|
96
|
+
return [row];
|
|
97
|
+
});
|
|
98
|
+
const checked = Array.isArray(output.rulesEvaluated)
|
|
99
|
+
? output.rulesEvaluated.flatMap((rule) => {
|
|
100
|
+
const text = clip(rule, 64);
|
|
101
|
+
return text ? [text] : [];
|
|
102
|
+
})
|
|
103
|
+
: [];
|
|
104
|
+
if (input.tool === "review_application_architecture") {
|
|
105
|
+
checked.unshift(`Reviewed ${input.submittedPaths.length} files.`);
|
|
106
|
+
checked.push(...input.submittedPaths);
|
|
107
|
+
}
|
|
108
|
+
const skipped = [];
|
|
109
|
+
if (Array.isArray(output.warnings) && output.warnings.length > 0) {
|
|
110
|
+
const count = output.warnings.length;
|
|
111
|
+
skipped.push(`The scanner reported ${count} ${count === 1 ? "warning" : "warnings"}.`);
|
|
112
|
+
}
|
|
113
|
+
for (const field of ["evidenceGaps", "limitations"]) {
|
|
114
|
+
if (!Array.isArray(output[field]))
|
|
115
|
+
continue;
|
|
116
|
+
for (const entry of output[field]) {
|
|
117
|
+
const text = clip(entry);
|
|
118
|
+
if (text)
|
|
119
|
+
skipped.push(text);
|
|
120
|
+
if (skipped.length >= 20)
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const summary = clip(output.summary);
|
|
125
|
+
const riskSentence = summary
|
|
126
|
+
?? (projected.length === 0
|
|
127
|
+
? "No findings. That does not prove the system is secure."
|
|
128
|
+
: projected[0].title);
|
|
129
|
+
return {
|
|
130
|
+
tool: input.tool,
|
|
131
|
+
engineVersion: VERSION,
|
|
132
|
+
target: input.target ?? null,
|
|
133
|
+
riskSentence,
|
|
134
|
+
findings: projected,
|
|
135
|
+
truncated: output.truncated === true || findings.length > 100,
|
|
136
|
+
checked: checked.length > 0 ? checked.slice(0, 20) : ["Individual checks were not recorded."],
|
|
137
|
+
skipped: skipped.length > 0 ? skipped.slice(0, 20) : ["Nothing was recorded as skipped."],
|
|
138
|
+
submittedPaths: input.submittedPaths.length > 0 ? input.submittedPaths : [fallbackPath],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function authorizationHeader() {
|
|
142
|
+
const fromRequest = requestAuthorization.getStore();
|
|
143
|
+
if (fromRequest)
|
|
144
|
+
return fromRequest;
|
|
145
|
+
const token = await getApiAccessToken();
|
|
146
|
+
return token ? `Bearer ${token}` : null;
|
|
147
|
+
}
|
|
148
|
+
export async function withSavedReview(output, input) {
|
|
149
|
+
const projection = buildProjection(input, output);
|
|
150
|
+
let savedReview = refused;
|
|
151
|
+
const authorization = await authorizationHeader();
|
|
152
|
+
if (authorization) {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(`${getApiUrl()}/api/mcp/reviews`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: authorization,
|
|
158
|
+
Accept: "application/json",
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(projection),
|
|
162
|
+
signal: AbortSignal.timeout(10_000),
|
|
163
|
+
redirect: "error",
|
|
164
|
+
cache: "no-store",
|
|
165
|
+
});
|
|
166
|
+
if (response.ok) {
|
|
167
|
+
const body = await response.json();
|
|
168
|
+
if (body.savedReview)
|
|
169
|
+
savedReview = body.savedReview;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
savedReview = refused;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { ...output, savedReview };
|
|
177
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { looksLikeIac, ScanInputError } from "./iac/parse.js";
|
|
|
6
6
|
import { filterFindings, scanIac, summarize } from "./iac/scan.js";
|
|
7
7
|
import { registerConsultation } from "./application/tools.js";
|
|
8
8
|
import { presentReview } from "./review-edits.js";
|
|
9
|
+
import { savedTargetSchema, withSavedReview } from "./saved-review.js";
|
|
9
10
|
import { VERSION } from "./version.js";
|
|
10
11
|
const contentSchema = z
|
|
11
12
|
.string()
|
|
@@ -93,8 +94,9 @@ export function createServer(remote = false) {
|
|
|
93
94
|
.optional()
|
|
94
95
|
.default("auto")
|
|
95
96
|
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, gitlab, azure-pipelines, cloudbuild, tfvars, serverless, iam, package, application, auto."),
|
|
97
|
+
target: savedTargetSchema,
|
|
96
98
|
},
|
|
97
|
-
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
99
|
+
}, async ({ content, focus, format, minSeverity, maxFindings, target }) => {
|
|
98
100
|
const botHints = fetchPublicBotHints();
|
|
99
101
|
if (!remote) {
|
|
100
102
|
const auth = await requireApiAuth();
|
|
@@ -103,15 +105,20 @@ export function createServer(remote = false) {
|
|
|
103
105
|
}
|
|
104
106
|
try {
|
|
105
107
|
const options = { focus, minSeverity, maxFindings };
|
|
108
|
+
const save = {
|
|
109
|
+
tool: "review_architecture",
|
|
110
|
+
target: target ?? null,
|
|
111
|
+
submittedPaths: [target?.path ?? ""],
|
|
112
|
+
};
|
|
106
113
|
if (format !== "application" &&
|
|
107
114
|
(format !== "auto" || looksLikeIac(content))) {
|
|
108
|
-
return result(presentReview(content, {
|
|
115
|
+
return result(await withSavedReview(presentReview(content, {
|
|
109
116
|
...scanIac(content, { ...options, format }),
|
|
110
117
|
botHints: await botHints,
|
|
111
|
-
}));
|
|
118
|
+
}), save));
|
|
112
119
|
}
|
|
113
120
|
const findings = filterFindings(architectureFindings(content, focus).slice(1), options);
|
|
114
|
-
return result(presentReview(content, {
|
|
121
|
+
return result(await withSavedReview(presentReview(content, {
|
|
115
122
|
status: "completed",
|
|
116
123
|
engineVersion: VERSION,
|
|
117
124
|
format: "application",
|
|
@@ -125,7 +132,7 @@ export function createServer(remote = false) {
|
|
|
125
132
|
"Infrastructure bottleneck checks cover a disabled timeout, a one-connection pool, and an unbounded retry limit.",
|
|
126
133
|
],
|
|
127
134
|
botHints: await botHints,
|
|
128
|
-
}));
|
|
135
|
+
}), save));
|
|
129
136
|
}
|
|
130
137
|
catch (error) {
|
|
131
138
|
return inputError(error);
|
|
@@ -172,15 +179,20 @@ export function createServer(remote = false) {
|
|
|
172
179
|
])
|
|
173
180
|
.default("auto"),
|
|
174
181
|
...filters,
|
|
182
|
+
target: savedTargetSchema,
|
|
175
183
|
},
|
|
176
|
-
}, async ({ content, format, focus, minSeverity, maxFindings }) => {
|
|
184
|
+
}, async ({ content, format, focus, minSeverity, maxFindings, target }) => {
|
|
177
185
|
if (!remote) {
|
|
178
186
|
const auth = await requireApiAuth();
|
|
179
187
|
if (!auth.ok)
|
|
180
188
|
return unauthorizedContent(auth);
|
|
181
189
|
}
|
|
182
190
|
try {
|
|
183
|
-
return result(presentReview(content, scanIac(content, { format, focus, minSeverity, maxFindings }))
|
|
191
|
+
return result(await withSavedReview(presentReview(content, scanIac(content, { format, focus, minSeverity, maxFindings })), {
|
|
192
|
+
tool: name,
|
|
193
|
+
target: target ?? null,
|
|
194
|
+
submittedPaths: [target?.path ?? ""],
|
|
195
|
+
}));
|
|
184
196
|
}
|
|
185
197
|
catch (error) {
|
|
186
198
|
return inputError(error);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.19";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.19";
|