@skaleagents/swarm 0.6.16 → 0.6.17
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 +16 -7
- package/dist/http.js +1 -1
- package/dist/review-edits.d.ts +21 -0
- package/dist/review-edits.js +145 -0
- package/dist/server.js +6 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# @skaleagents/swarm
|
|
2
2
|
|
|
3
|
-
SkaleAgents MCP server
|
|
4
|
-
|
|
3
|
+
SkaleAgents MCP server. The hosted connection is `https://skaleagents.com/mcp`.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
Copy `https://skaleagents.com/mcp` into your client and choose OAuth. Sign in
|
|
8
|
+
with Google and click Allow. Leave client ID and secret fields blank when the
|
|
9
|
+
client asks. Setup steps for Cursor, Claude Code, Claude Desktop, ChatGPT, and
|
|
10
|
+
Codex are on the [MCP config page](https://skaleagents.com/mcp-config).
|
|
11
|
+
|
|
12
|
+
The hosted URL is the way to connect. A local stdio process can still run this
|
|
13
|
+
package; it opens the browser for OAuth when no token is set.
|
|
5
14
|
|
|
6
15
|
Tools: `plan_architecture_review`, `review_application_architecture`,
|
|
7
16
|
`review_architecture`, and `scan_iac`. The older `scan_iac_stub` name remains
|
|
@@ -56,7 +65,7 @@ for every review area, and the next questions. The MCP provides static evidence
|
|
|
56
65
|
and the connected assistant's model reasons through the architecture. No
|
|
57
66
|
separate hosted model is invoked, and the tools do not clone repositories or
|
|
58
67
|
run submitted code. A clean static check is not a whole-system correctness
|
|
59
|
-
verdict. The [
|
|
68
|
+
verdict. The [MCP config page](https://skaleagents.com/mcp-config) has an interactive
|
|
60
69
|
review-brief builder.
|
|
61
70
|
|
|
62
71
|
## Infrastructure scanning
|
|
@@ -106,8 +115,8 @@ in every result. An empty finding list is not proof that a system is secure.
|
|
|
106
115
|
|
|
107
116
|
Use `https://skaleagents.com/mcp` in Claude Desktop or ChatGPT's custom connector
|
|
108
117
|
settings. Choose OAuth and leave client ID and secret fields blank. The client
|
|
109
|
-
registers itself, opens Google sign-in, and asks you to
|
|
110
|
-
See [client setup](https://skaleagents.com/
|
|
118
|
+
registers itself, opens Google sign-in, and asks you to click Allow.
|
|
119
|
+
See [client setup](https://skaleagents.com/mcp-config) for Cursor, Claude Code,
|
|
111
120
|
Claude Desktop, ChatGPT, and Codex instructions.
|
|
112
121
|
|
|
113
122
|
The web app hosts this endpoint using `handleMcpRequest` from
|
|
@@ -127,8 +136,8 @@ forwarded to the public bot directory.
|
|
|
127
136
|
|
|
128
137
|
The first tool call opens browser sign-in. Approve MCP access there and return
|
|
129
138
|
to your AI client. The package stores the OAuth refresh credential locally and
|
|
130
|
-
refreshes access automatically.
|
|
131
|
-
|
|
139
|
+
refreshes access automatically. Active connections can be revoked from the
|
|
140
|
+
[MCP config page](https://skaleagents.com/mcp-config).
|
|
132
141
|
|
|
133
142
|
Local `review_architecture` calls validate the account and fetch public bot
|
|
134
143
|
hints concurrently. The public directory request carries no credential. Every
|
package/dist/http.js
CHANGED
|
@@ -33,7 +33,7 @@ export function protectedResourceMetadata() {
|
|
|
33
33
|
authorization_servers: [getApiUrl()],
|
|
34
34
|
scopes_supported: ["mcp"],
|
|
35
35
|
bearer_methods_supported: ["header"],
|
|
36
|
-
resource_documentation: "https://skaleagents.com/
|
|
36
|
+
resource_documentation: "https://skaleagents.com/mcp-config",
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
export async function handleMcpRequest(request) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ExactLineEdit = {
|
|
2
|
+
diff: string;
|
|
3
|
+
from: string;
|
|
4
|
+
to: string | null;
|
|
5
|
+
};
|
|
6
|
+
export declare function exactLineEdit(content: string, title: string): ExactLineEdit | null;
|
|
7
|
+
export declare function rankFindings<T extends {
|
|
8
|
+
severity: string;
|
|
9
|
+
}>(findings: T[]): T[];
|
|
10
|
+
export declare function changePhrase(edit: ExactLineEdit): string;
|
|
11
|
+
type RankedFinding = {
|
|
12
|
+
title: string;
|
|
13
|
+
severity: string;
|
|
14
|
+
proposedChange?: ExactLineEdit;
|
|
15
|
+
};
|
|
16
|
+
export declare function presentReview(content: string, output: Record<string, unknown>): {
|
|
17
|
+
findings: any[];
|
|
18
|
+
priority: RankedFinding[];
|
|
19
|
+
summary: string;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const severityOrder = ["critical", "high", "medium", "low", "info"];
|
|
2
|
+
const noFindingsSummary = "No findings. That does not prove the system is secure.";
|
|
3
|
+
const rules = {
|
|
4
|
+
"Dockerfile runs as root": dockerfileUser,
|
|
5
|
+
"Workflow permissions are broad": workflowPermissions,
|
|
6
|
+
"Elevated container privileges": privilegedContainer,
|
|
7
|
+
"Privileged container in a Helm template": privilegedContainer,
|
|
8
|
+
"Database publicly accessible": publicDatabase,
|
|
9
|
+
"Database or storage allows public network access": publicNetworkAccess,
|
|
10
|
+
"Storage encryption disabled": storageEncryption,
|
|
11
|
+
"Pull request target checks out PR code": pullRequestTargetCheckout,
|
|
12
|
+
};
|
|
13
|
+
export function exactLineEdit(content, title) {
|
|
14
|
+
const rule = rules[title];
|
|
15
|
+
if (!rule)
|
|
16
|
+
return null;
|
|
17
|
+
return rule(content.split("\n"));
|
|
18
|
+
}
|
|
19
|
+
export function rankFindings(findings) {
|
|
20
|
+
return findings
|
|
21
|
+
.map((finding, index) => ({ finding, index }))
|
|
22
|
+
.sort((left, right) => {
|
|
23
|
+
const leftRank = severityOrder.indexOf(left.finding.severity);
|
|
24
|
+
const rightRank = severityOrder.indexOf(right.finding.severity);
|
|
25
|
+
return (leftRank === -1 ? severityOrder.length : leftRank) - (rightRank === -1 ? severityOrder.length : rightRank) || left.index - right.index;
|
|
26
|
+
})
|
|
27
|
+
.map((item) => item.finding);
|
|
28
|
+
}
|
|
29
|
+
export function changePhrase(edit) {
|
|
30
|
+
if (edit.to === null)
|
|
31
|
+
return `${edit.from.trim()} removed`;
|
|
32
|
+
return `${edit.from.trim()} becomes ${edit.to.trim()}`;
|
|
33
|
+
}
|
|
34
|
+
function isRankedFinding(value) {
|
|
35
|
+
if (!value || typeof value !== "object")
|
|
36
|
+
return false;
|
|
37
|
+
const finding = value;
|
|
38
|
+
return typeof finding.title === "string" && typeof finding.severity === "string";
|
|
39
|
+
}
|
|
40
|
+
export function presentReview(content, output) {
|
|
41
|
+
const raw = Array.isArray(output.findings) ? output.findings : [];
|
|
42
|
+
const findings = raw.map((finding) => {
|
|
43
|
+
if (!isRankedFinding(finding))
|
|
44
|
+
return finding;
|
|
45
|
+
const edit = exactLineEdit(content, finding.title);
|
|
46
|
+
if (!edit)
|
|
47
|
+
return finding;
|
|
48
|
+
return { ...finding, proposedChange: edit };
|
|
49
|
+
});
|
|
50
|
+
const priority = rankFindings(findings.filter(isRankedFinding)).slice(0, 3);
|
|
51
|
+
return {
|
|
52
|
+
...output,
|
|
53
|
+
findings,
|
|
54
|
+
priority,
|
|
55
|
+
summary: priority.length === 0
|
|
56
|
+
? noFindingsSummary
|
|
57
|
+
: `Fix these first: ${priority.map((finding) => finding.proposedChange ? `${finding.title}: ${changePhrase(finding.proposedChange)}` : finding.title).join(", ")}.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function dockerfileUser(lines) {
|
|
61
|
+
const exact = lines.flatMap((line, index) => line === "USER root" || line === "USER 0" ? [{ line, index }] : []);
|
|
62
|
+
if (exact.length !== 1)
|
|
63
|
+
return null;
|
|
64
|
+
return replacement(exact[0].line, "USER nobody", exact[0].index);
|
|
65
|
+
}
|
|
66
|
+
function workflowPermissions(lines) {
|
|
67
|
+
const exact = lines.flatMap((line, index) => line.trim() === "permissions: write-all" ? [{ line, index }] : []);
|
|
68
|
+
if (exact.length !== 1)
|
|
69
|
+
return null;
|
|
70
|
+
const indent = exact[0].line.match(/^\s*/)?.[0] ?? "";
|
|
71
|
+
return replacement(exact[0].line, `${indent}permissions: contents: read`, exact[0].index);
|
|
72
|
+
}
|
|
73
|
+
function privilegedContainer(lines) {
|
|
74
|
+
return oneExactReplacement(lines, [{ from: "privileged: true", to: "privileged: false" }]);
|
|
75
|
+
}
|
|
76
|
+
function publicDatabase(lines) {
|
|
77
|
+
return oneExactReplacement(lines, [{ from: "publicly_accessible = true", to: "publicly_accessible = false" }]);
|
|
78
|
+
}
|
|
79
|
+
function publicNetworkAccess(lines) {
|
|
80
|
+
return oneExactReplacement(lines, [
|
|
81
|
+
{ from: "publicNetworkAccess: 'Enabled'", to: "publicNetworkAccess: 'Disabled'" },
|
|
82
|
+
{ from: "publicNetworkAccess: \"Enabled\"", to: "publicNetworkAccess: \"Disabled\"" },
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
function storageEncryption(lines) {
|
|
86
|
+
return oneExactReplacement(lines, [
|
|
87
|
+
{ from: "storage_encrypted = false", to: "storage_encrypted = true" },
|
|
88
|
+
{ from: "encrypted: false", to: "encrypted: true" },
|
|
89
|
+
{ from: "encrypted = false", to: "encrypted = true" },
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
function pullRequestTargetCheckout(lines) {
|
|
93
|
+
const exact = lines.flatMap((line, index) => isExactHeadRefLine(line) ? [{ line, index }] : []);
|
|
94
|
+
if (exact.length !== 1)
|
|
95
|
+
return null;
|
|
96
|
+
const match = exact[0];
|
|
97
|
+
if (!isPullRequestTargetWorkflow(lines) || !refBelongsToCheckout(lines, match.index))
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
from: match.line,
|
|
101
|
+
to: null,
|
|
102
|
+
diff: [
|
|
103
|
+
`@@ -${match.index + 1},1 +${Math.max(match.index, 0)},0 @@`,
|
|
104
|
+
`-${match.line}`,
|
|
105
|
+
].join("\n"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function oneExactReplacement(lines, pairs) {
|
|
109
|
+
const matches = lines.flatMap((line, index) => {
|
|
110
|
+
const pair = pairs.find((item) => item.from === line.trim());
|
|
111
|
+
return pair ? [{ line, index, to: pair.to }] : [];
|
|
112
|
+
});
|
|
113
|
+
if (matches.length !== 1)
|
|
114
|
+
return null;
|
|
115
|
+
const indent = matches[0].line.match(/^\s*/)?.[0] ?? "";
|
|
116
|
+
return replacement(matches[0].line, `${indent}${matches[0].to}`, matches[0].index);
|
|
117
|
+
}
|
|
118
|
+
function replacement(from, to, lineIndex) {
|
|
119
|
+
const line = lineIndex + 1;
|
|
120
|
+
return {
|
|
121
|
+
from,
|
|
122
|
+
to,
|
|
123
|
+
diff: [`@@ -${line},1 +${line},1 @@`, `-${from}`, `+${to}`].join("\n"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function isExactHeadRefLine(line) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
return trimmed === "ref: ${{ github.event.pull_request.head.ref }}" || trimmed === "ref: ${{ github.event.pull_request.head.sha }}";
|
|
129
|
+
}
|
|
130
|
+
function isPullRequestTargetWorkflow(lines) {
|
|
131
|
+
return lines.some((line) => {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (trimmed.startsWith("#"))
|
|
134
|
+
return false;
|
|
135
|
+
return trimmed === "on: pull_request_target" || trimmed === "pull_request_target:" || trimmed.startsWith("pull_request_target:");
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function refBelongsToCheckout(lines, index) {
|
|
139
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
140
|
+
const trimmed = lines[cursor].trim();
|
|
141
|
+
if (trimmed.startsWith("- uses:") || trimmed.startsWith("uses:"))
|
|
142
|
+
return /actions\/checkout@/.test(trimmed);
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { architectureFindings, fetchPublicBotHints } from "./review.js";
|
|
|
5
5
|
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
|
+
import { presentReview } from "./review-edits.js";
|
|
8
9
|
import { VERSION } from "./version.js";
|
|
9
10
|
const contentSchema = z
|
|
10
11
|
.string()
|
|
@@ -104,18 +105,17 @@ export function createServer(remote = false) {
|
|
|
104
105
|
const options = { focus, minSeverity, maxFindings };
|
|
105
106
|
if (format !== "application" &&
|
|
106
107
|
(format !== "auto" || looksLikeIac(content))) {
|
|
107
|
-
return result({
|
|
108
|
+
return result(presentReview(content, {
|
|
108
109
|
...scanIac(content, { ...options, format }),
|
|
109
110
|
botHints: await botHints,
|
|
110
|
-
});
|
|
111
|
+
}));
|
|
111
112
|
}
|
|
112
113
|
const findings = filterFindings(architectureFindings(content, focus).slice(1), options);
|
|
113
|
-
return result({
|
|
114
|
+
return result(presentReview(content, {
|
|
114
115
|
status: "completed",
|
|
115
116
|
engineVersion: VERSION,
|
|
116
117
|
format: "application",
|
|
117
118
|
focus,
|
|
118
|
-
summary: `Application review completed; ${findings.length} findings match the selected filters.`,
|
|
119
119
|
findings: findings.slice(0, maxFindings),
|
|
120
120
|
totalFindings: findings.length,
|
|
121
121
|
totals: summarize(findings),
|
|
@@ -125,7 +125,7 @@ export function createServer(remote = false) {
|
|
|
125
125
|
"Infrastructure bottleneck checks cover a disabled timeout, a one-connection pool, and an unbounded retry limit.",
|
|
126
126
|
],
|
|
127
127
|
botHints: await botHints,
|
|
128
|
-
});
|
|
128
|
+
}));
|
|
129
129
|
}
|
|
130
130
|
catch (error) {
|
|
131
131
|
return inputError(error);
|
|
@@ -180,7 +180,7 @@ export function createServer(remote = false) {
|
|
|
180
180
|
return unauthorizedContent(auth);
|
|
181
181
|
}
|
|
182
182
|
try {
|
|
183
|
-
return result(scanIac(content, { format, focus, minSeverity, maxFindings }));
|
|
183
|
+
return result(presentReview(content, scanIac(content, { format, focus, minSeverity, maxFindings })));
|
|
184
184
|
}
|
|
185
185
|
catch (error) {
|
|
186
186
|
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.17";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.17";
|