@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 CHANGED
@@ -1,7 +1,16 @@
1
1
  # @skaleagents/swarm
2
2
 
3
- SkaleAgents MCP server with local stdio and hosted Streamable HTTP transports.
4
- Uses browser OAuth for sign-in.
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 [settings page](https://skaleagents.com/settings) has an interactive
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 approve MCP access.
110
- See [client setup](https://skaleagents.com/settings) for Cursor, Claude Code,
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. You do not need to create or paste an API key.
131
- Active connections can be revoked from the web app's MCP settings page.
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/settings",
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.16";
1
+ export declare const VERSION = "0.6.17";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.16";
1
+ export const VERSION = "0.6.17";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.6.16",
3
+ "version": "0.6.17",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"