@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.
@@ -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
- async function run(remote, compute) {
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 output = compute();
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
- const server = createServer(true);
109
- const transport = new WebStandardStreamableHTTPServerTransport({
110
- sessionIdGenerator: undefined,
111
- enableJsonResponse: true,
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
- // Finish the JSON response before closing this request-scoped transport.
119
- const responseBody = await response.arrayBuffer();
120
- const responseHeaders = new Headers(response.headers);
121
- for (const [key, value] of Object.entries(headers))
122
- responseHeaders.set(key, value);
123
- return new Response(responseBody.byteLength ? responseBody : null, {
124
- status: response.status,
125
- headers: responseHeaders,
126
- });
127
- }
128
- catch {
129
- return json(500, { error: "mcp_request_failed" });
130
- }
131
- finally {
132
- await server.close();
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.18";
1
+ export declare const VERSION = "0.6.19";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.18";
1
+ export const VERSION = "0.6.19";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.6.18",
3
+ "version": "0.6.19",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"