n8n-mcp-community 0.1.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/CHANGELOG.md +116 -0
- package/CODE_OF_CONDUCT.md +51 -0
- package/CONTRIBUTING.md +69 -0
- package/LICENSE +21 -0
- package/README.md +271 -0
- package/ROADMAP.md +41 -0
- package/SECURITY.md +53 -0
- package/SUPPORT.md +19 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/dist/config.js +76 -0
- package/dist/content/node-docs.js +47 -0
- package/dist/content/official-urls.js +11 -0
- package/dist/doctor.js +88 -0
- package/dist/index.js +119 -0
- package/dist/introspect/collector.js +642 -0
- package/dist/introspect/contracts.js +309 -0
- package/dist/introspect/engine.js +184 -0
- package/dist/introspect/http-client.js +142 -0
- package/dist/introspect/index.js +5 -0
- package/dist/introspect/order.js +3 -0
- package/dist/introspect/render.js +25 -0
- package/dist/introspect/rule-ids.js +25 -0
- package/dist/introspect/rules.js +905 -0
- package/dist/introspect/sanitize.js +66 -0
- package/dist/n8n/client.js +170 -0
- package/dist/prompts.js +69 -0
- package/dist/resources.js +42 -0
- package/dist/security/operation-policy.js +20 -0
- package/dist/security/redaction.js +253 -0
- package/dist/server.js +16 -0
- package/dist/tools/common.js +54 -0
- package/dist/tools/credentials.js +259 -0
- package/dist/tools/definition.js +169 -0
- package/dist/tools/endpoint-contracts.js +53 -0
- package/dist/tools/executions.js +180 -0
- package/dist/tools/registry.js +24 -0
- package/dist/tools/schemas.js +203 -0
- package/dist/tools/tags.js +73 -0
- package/dist/tools/users.js +130 -0
- package/dist/tools/utilities.js +347 -0
- package/dist/tools/workflows.js +771 -0
- package/dist/version.js +1 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +122 -0
- package/docs/assets/demo.gif +0 -0
- package/docs/clients.md +95 -0
- package/docs/compatibility.md +112 -0
- package/docs/configuration.md +87 -0
- package/docs/demo-transcript.md +31 -0
- package/docs/examples.md +86 -0
- package/docs/faq.md +90 -0
- package/docs/getting-started.md +93 -0
- package/docs/installation.md +120 -0
- package/docs/provenance.md +152 -0
- package/docs/security-model.md +170 -0
- package/docs/tools.md +937 -0
- package/docs/troubleshooting.md +185 -0
- package/package.json +126 -0
- package/server.json +51 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { ExecutionDetailResponseSchema, ExecutionListResponseSchema, WorkflowResponseSchema, } from "./contracts.js";
|
|
2
|
+
import { compareCodeUnits } from "./order.js";
|
|
3
|
+
import { fingerprintError, optionalLabel } from "./sanitize.js";
|
|
4
|
+
export const PROFILE_BUDGETS = Object.freeze({
|
|
5
|
+
quick: Object.freeze({
|
|
6
|
+
workflowRequests: 1,
|
|
7
|
+
listPages: 1,
|
|
8
|
+
detailRequests: 0,
|
|
9
|
+
metadataExecutions: 25,
|
|
10
|
+
requestTimeoutMs: 5_000,
|
|
11
|
+
totalDeadlineMs: 10_000,
|
|
12
|
+
workflowBytes: 1_048_576,
|
|
13
|
+
listBytes: 524_288,
|
|
14
|
+
detailBytes: 0,
|
|
15
|
+
totalBytes: 1_572_864,
|
|
16
|
+
structuredOutputBytes: 98_304,
|
|
17
|
+
combinedOutputBytes: 229_376,
|
|
18
|
+
}),
|
|
19
|
+
deep: Object.freeze({
|
|
20
|
+
workflowRequests: 1,
|
|
21
|
+
listPages: 4,
|
|
22
|
+
detailRequests: 4,
|
|
23
|
+
metadataExecutions: 100,
|
|
24
|
+
requestTimeoutMs: 5_000,
|
|
25
|
+
totalDeadlineMs: 25_000,
|
|
26
|
+
workflowBytes: 1_048_576,
|
|
27
|
+
listBytes: 524_288,
|
|
28
|
+
detailBytes: 524_288,
|
|
29
|
+
totalBytes: 5_242_880,
|
|
30
|
+
structuredOutputBytes: 131_072,
|
|
31
|
+
combinedOutputBytes: 327_680,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
export class IntrospectCollectionError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
status;
|
|
37
|
+
constructor(code, message, status) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.name = "IntrospectCollectionError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const SAFE_LIMITATIONS = Object.freeze({
|
|
45
|
+
deadline_exceeded: "The Introspect deadline was reached before collection completed.",
|
|
46
|
+
response_too_large: "An upstream response exceeded the immutable Introspect byte limit.",
|
|
47
|
+
upstream_http_error: "A public n8n API request failed.",
|
|
48
|
+
invalid_json: "A public n8n API response was not valid JSON.",
|
|
49
|
+
invalid_schema: "A public n8n API response did not match the supported schema.",
|
|
50
|
+
repeated_cursor: "Pagination stopped because n8n returned a cursor that was already seen.",
|
|
51
|
+
invalid_timestamp: "Executions with missing or invalid start timestamps were excluded from the lookback sample.",
|
|
52
|
+
invalid_execution_id: "An execution detail was skipped because its identifier was not a safe path segment.",
|
|
53
|
+
page_failed: "A later execution metadata page could not be collected.",
|
|
54
|
+
detail_failed: "One or more selected execution details could not be reduced safely.",
|
|
55
|
+
ordering_unreliable: "Execution pages were not consistently ordered newest first; temporal rules are inconclusive.",
|
|
56
|
+
finding_limit: "One or more rules produced more occurrences than the immutable output cap allows.",
|
|
57
|
+
output_limit: "The report was reduced to remain within the immutable output byte limit.",
|
|
58
|
+
label_redaction_limit: "Pattern redaction cannot recognize every sensitive proper name; opaque identifiers remain authoritative.",
|
|
59
|
+
});
|
|
60
|
+
function limitation(code) {
|
|
61
|
+
return { code, message: SAFE_LIMITATIONS[code] };
|
|
62
|
+
}
|
|
63
|
+
function errorCode(error) {
|
|
64
|
+
if (!(error instanceof IntrospectCollectionError))
|
|
65
|
+
return undefined;
|
|
66
|
+
return error.code in SAFE_LIMITATIONS
|
|
67
|
+
? error.code
|
|
68
|
+
: "upstream_http_error";
|
|
69
|
+
}
|
|
70
|
+
function parseOrThrow(schema, value) {
|
|
71
|
+
const parsed = schema.safeParse(value);
|
|
72
|
+
if (!parsed.success) {
|
|
73
|
+
throw new IntrospectCollectionError("invalid_schema", "The public n8n API response did not match the supported schema.");
|
|
74
|
+
}
|
|
75
|
+
return parsed.data;
|
|
76
|
+
}
|
|
77
|
+
function timestampMs(value) {
|
|
78
|
+
if (!value)
|
|
79
|
+
return undefined;
|
|
80
|
+
const parsed = Date.parse(value);
|
|
81
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
82
|
+
}
|
|
83
|
+
function normalizedTimestamp(value) {
|
|
84
|
+
const parsed = timestampMs(value);
|
|
85
|
+
return parsed === undefined ? null : new Date(parsed).toISOString();
|
|
86
|
+
}
|
|
87
|
+
function executionDuration(execution) {
|
|
88
|
+
const start = timestampMs(execution.startedAt);
|
|
89
|
+
const stop = timestampMs(execution.stoppedAt);
|
|
90
|
+
if (start === undefined || stop === undefined || stop < start)
|
|
91
|
+
return undefined;
|
|
92
|
+
return stop - start;
|
|
93
|
+
}
|
|
94
|
+
function normalizeStatus(status) {
|
|
95
|
+
switch (status.toLowerCase()) {
|
|
96
|
+
case "success":
|
|
97
|
+
case "error":
|
|
98
|
+
case "crashed":
|
|
99
|
+
case "canceled":
|
|
100
|
+
case "running":
|
|
101
|
+
case "waiting":
|
|
102
|
+
case "new":
|
|
103
|
+
return status.toLowerCase();
|
|
104
|
+
default:
|
|
105
|
+
return "unknown";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function normalizeMode(mode) {
|
|
109
|
+
if (!mode || !/^[A-Za-z][A-Za-z0-9_-]{0,39}$/.test(mode))
|
|
110
|
+
return "unknown";
|
|
111
|
+
return mode.toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
function reduceExecution(raw) {
|
|
114
|
+
return {
|
|
115
|
+
id: raw.id,
|
|
116
|
+
status: normalizeStatus(raw.status),
|
|
117
|
+
mode: normalizeMode(raw.mode),
|
|
118
|
+
startedAt: normalizedTimestamp(raw.startedAt),
|
|
119
|
+
stoppedAt: normalizedTimestamp(raw.stoppedAt),
|
|
120
|
+
waitObserved: raw.waitTill !== undefined && raw.waitTill !== null && raw.waitTill !== "",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function assessOrdering(executions) {
|
|
124
|
+
if (executions.length === 0)
|
|
125
|
+
return "unknown";
|
|
126
|
+
const timestamps = executions.map((execution) => timestampMs(execution.startedAt));
|
|
127
|
+
if (timestamps.some((value) => value === undefined))
|
|
128
|
+
return "unknown";
|
|
129
|
+
for (let index = 1; index < timestamps.length; index += 1) {
|
|
130
|
+
if (timestamps[index] > timestamps[index - 1])
|
|
131
|
+
return "unreliable";
|
|
132
|
+
}
|
|
133
|
+
return "verified_newest_first";
|
|
134
|
+
}
|
|
135
|
+
function safeExecutionId(id) {
|
|
136
|
+
return /^[A-Za-z0-9_-]{1,128}$/.test(id);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A string value counts as a literal secret when it is a real value rather than an
|
|
140
|
+
* n8n expression (`=` / `={{ ... }}`), an environment reference, or an obvious
|
|
141
|
+
* placeholder (asterisks, `[REDACTED]`-style tokens, or `<...>` templates). Only the
|
|
142
|
+
* boolean verdict is used; the value itself is never retained.
|
|
143
|
+
*/
|
|
144
|
+
function isLiteralSecretValue(value) {
|
|
145
|
+
const trimmed = value.trim();
|
|
146
|
+
return (trimmed !== "" &&
|
|
147
|
+
!trimmed.startsWith("=") &&
|
|
148
|
+
!trimmed.includes("$env") &&
|
|
149
|
+
!/^\*+$/.test(trimmed) &&
|
|
150
|
+
!/^\[[A-Z_-]+\]$/.test(trimmed) &&
|
|
151
|
+
!/^<[^>]+>$/.test(trimmed));
|
|
152
|
+
}
|
|
153
|
+
function analyzeParameters(value, maxEntries = 1_000) {
|
|
154
|
+
const seen = new WeakSet();
|
|
155
|
+
const references = new Set();
|
|
156
|
+
const stack = [{ key: "", value }];
|
|
157
|
+
const secretKey = /(password|passwd|secret|token|api.?key|authorization|private.?key|client.?secret)/i;
|
|
158
|
+
let entries = 0;
|
|
159
|
+
let hasIdempotencyHeader = false;
|
|
160
|
+
let idempotencyHeaderMissingValue = false;
|
|
161
|
+
let literalSecretCount = 0;
|
|
162
|
+
while (stack.length > 0 && entries < maxEntries) {
|
|
163
|
+
const current = stack.pop();
|
|
164
|
+
entries += 1;
|
|
165
|
+
const normalizedKey = current.key.toLowerCase().replace(/[_\s]/g, "-");
|
|
166
|
+
const currentIsIdempotencyHeader = normalizedKey === "idempotency-key" || normalizedKey === "x-idempotency-key";
|
|
167
|
+
if (currentIsIdempotencyHeader && typeof current.value === "string") {
|
|
168
|
+
if (current.value.trim() === "")
|
|
169
|
+
idempotencyHeaderMissingValue = true;
|
|
170
|
+
else
|
|
171
|
+
hasIdempotencyHeader = true;
|
|
172
|
+
}
|
|
173
|
+
if (current.value && typeof current.value === "object" && !Array.isArray(current.value)) {
|
|
174
|
+
const record = current.value;
|
|
175
|
+
const headerName = [record.name, record.key].find((candidate) => typeof candidate === "string");
|
|
176
|
+
const normalizedHeaderName = headerName?.toLowerCase().replace(/[_\s]/g, "-");
|
|
177
|
+
if (normalizedHeaderName === "idempotency-key" ||
|
|
178
|
+
normalizedHeaderName === "x-idempotency-key") {
|
|
179
|
+
if (typeof record.value === "string" && record.value.trim() !== "") {
|
|
180
|
+
hasIdempotencyHeader = true;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
idempotencyHeaderMissingValue = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// n8n's canonical parameter-entry shape stores the secret-indicating name in a
|
|
187
|
+
// sibling `name`/`key` field and the literal in `value`
|
|
188
|
+
// (e.g. headerParameters.parameters: [{ name: "Authorization", value: "Bearer ..." }]).
|
|
189
|
+
if (headerName !== undefined &&
|
|
190
|
+
secretKey.test(headerName) &&
|
|
191
|
+
typeof record.value === "string" &&
|
|
192
|
+
isLiteralSecretValue(record.value)) {
|
|
193
|
+
literalSecretCount += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (typeof current.value === "string" &&
|
|
197
|
+
secretKey.test(current.key) &&
|
|
198
|
+
isLiteralSecretValue(current.value)) {
|
|
199
|
+
literalSecretCount += 1;
|
|
200
|
+
}
|
|
201
|
+
if (typeof current.value === "string" && current.value.includes("$")) {
|
|
202
|
+
const patterns = [
|
|
203
|
+
/\$node\[\s*["']([^"']{1,200})["']\s*\]/g,
|
|
204
|
+
/\$items\(\s*["']([^"']{1,200})["']/g,
|
|
205
|
+
/\$\(\s*["']([^"']{1,200})["']\s*\)/g,
|
|
206
|
+
];
|
|
207
|
+
for (const pattern of patterns) {
|
|
208
|
+
for (const match of current.value.matchAll(pattern)) {
|
|
209
|
+
const reference = match[1];
|
|
210
|
+
if (reference !== undefined)
|
|
211
|
+
references.add(reference);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!current.value || typeof current.value !== "object" || seen.has(current.value))
|
|
216
|
+
continue;
|
|
217
|
+
seen.add(current.value);
|
|
218
|
+
const children = Array.isArray(current.value)
|
|
219
|
+
? current.value.map((child, index) => [String(index), child])
|
|
220
|
+
: Object.entries(current.value);
|
|
221
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
222
|
+
const entry = children[index];
|
|
223
|
+
if (entry === undefined)
|
|
224
|
+
continue;
|
|
225
|
+
const [key, child] = entry;
|
|
226
|
+
stack.push({ key, value: child });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
references: [...references],
|
|
231
|
+
hasIdempotencyHeader,
|
|
232
|
+
idempotencyHeaderMissingValue,
|
|
233
|
+
literalSecretCount,
|
|
234
|
+
complete: stack.length === 0,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function literalSubworkflowId(node) {
|
|
238
|
+
if (node.type !== "n8n-nodes-base.executeWorkflow")
|
|
239
|
+
return undefined;
|
|
240
|
+
const raw = node.parameters?.workflowId;
|
|
241
|
+
if (typeof raw === "string")
|
|
242
|
+
return raw.startsWith("=") ? undefined : raw;
|
|
243
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
244
|
+
const value = raw.value;
|
|
245
|
+
if (typeof value === "string" && !value.startsWith("="))
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
function safeNodeType(type) {
|
|
251
|
+
if (!type || type.length > 160 || !/^[A-Za-z0-9_.@/-]+$/.test(type))
|
|
252
|
+
return undefined;
|
|
253
|
+
return type;
|
|
254
|
+
}
|
|
255
|
+
function reduceHttpMethod(value) {
|
|
256
|
+
if (typeof value !== "string")
|
|
257
|
+
return undefined;
|
|
258
|
+
const method = value.toUpperCase();
|
|
259
|
+
return ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)
|
|
260
|
+
? method
|
|
261
|
+
: "OTHER";
|
|
262
|
+
}
|
|
263
|
+
function timezoneState(value) {
|
|
264
|
+
if (value === undefined || value === "")
|
|
265
|
+
return "absent";
|
|
266
|
+
if (typeof value !== "string" || value.length > 100)
|
|
267
|
+
return "invalid";
|
|
268
|
+
try {
|
|
269
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value });
|
|
270
|
+
return "valid";
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return "invalid";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function reduceWorkflow(raw, input) {
|
|
277
|
+
if (raw.id !== input.workflowId) {
|
|
278
|
+
throw new IntrospectCollectionError("invalid_schema", "The workflow response ID did not match the request.");
|
|
279
|
+
}
|
|
280
|
+
const firstIndexByName = new Map();
|
|
281
|
+
let duplicateNames = 0;
|
|
282
|
+
raw.nodes.forEach((node, index) => {
|
|
283
|
+
if (firstIndexByName.has(node.name))
|
|
284
|
+
duplicateNames += 1;
|
|
285
|
+
else
|
|
286
|
+
firstIndexByName.set(node.name, index);
|
|
287
|
+
});
|
|
288
|
+
const nodes = raw.nodes.map((node, index) => {
|
|
289
|
+
const parameterFacts = analyzeParameters(node.parameters);
|
|
290
|
+
const references = parameterFacts.references;
|
|
291
|
+
const target = literalSubworkflowId(node);
|
|
292
|
+
const type = safeNodeType(node.type);
|
|
293
|
+
const label = optionalLabel(node.name, input.includeSanitizedLabels);
|
|
294
|
+
const httpMethod = type === "n8n-nodes-base.httpRequest" ? reduceHttpMethod(node.parameters?.method) : undefined;
|
|
295
|
+
let subworkflowTarget = "not_applicable";
|
|
296
|
+
if (type === "n8n-nodes-base.executeWorkflow") {
|
|
297
|
+
subworkflowTarget =
|
|
298
|
+
target === undefined
|
|
299
|
+
? "dynamic_or_missing"
|
|
300
|
+
: target === input.workflowId
|
|
301
|
+
? "self"
|
|
302
|
+
: "other";
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
ref: `node-${index + 1}`,
|
|
306
|
+
...(label ? { label } : {}),
|
|
307
|
+
...(type ? { type } : {}),
|
|
308
|
+
...(node.typeVersion !== undefined ? { typeVersion: node.typeVersion } : {}),
|
|
309
|
+
disabled: node.disabled === true,
|
|
310
|
+
retryOnFail: node.retryOnFail === true,
|
|
311
|
+
...(node.maxTries !== undefined ? { maxTries: node.maxTries } : {}),
|
|
312
|
+
...(node.waitBetweenTries !== undefined ? { waitBetweenTries: node.waitBetweenTries } : {}),
|
|
313
|
+
continueOnFail: node.continueOnFail === true,
|
|
314
|
+
...(node.onError === undefined ? {} : { onError: node.onError }),
|
|
315
|
+
webhookResponseMode: type === "n8n-nodes-base.webhook" && node.parameters?.responseMode === "responseNode",
|
|
316
|
+
exactExpressionReferenceCount: references.length,
|
|
317
|
+
missingExpressionReferenceCount: references.filter((name) => !firstIndexByName.has(name))
|
|
318
|
+
.length,
|
|
319
|
+
parameterScanComplete: parameterFacts.complete,
|
|
320
|
+
subworkflowTarget,
|
|
321
|
+
...(httpMethod === undefined ? {} : { httpMethod }),
|
|
322
|
+
hasIdempotencyHeader: parameterFacts.hasIdempotencyHeader,
|
|
323
|
+
idempotencyHeaderMissingValue: parameterFacts.idempotencyHeaderMissingValue,
|
|
324
|
+
literalSecretCount: parameterFacts.literalSecretCount,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
const edges = [];
|
|
328
|
+
let invalidEdges = 0;
|
|
329
|
+
let danglingSources = 0;
|
|
330
|
+
let danglingTargets = 0;
|
|
331
|
+
for (const [sourceName, outputGroups] of Object.entries(raw.connections)) {
|
|
332
|
+
const sourceIndex = firstIndexByName.get(sourceName);
|
|
333
|
+
if (sourceIndex === undefined) {
|
|
334
|
+
danglingSources += 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (!outputGroups || typeof outputGroups !== "object" || Array.isArray(outputGroups)) {
|
|
338
|
+
invalidEdges += 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
for (const branches of Object.values(outputGroups)) {
|
|
342
|
+
if (!Array.isArray(branches)) {
|
|
343
|
+
invalidEdges += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
for (const branch of branches) {
|
|
347
|
+
if (!Array.isArray(branch)) {
|
|
348
|
+
invalidEdges += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
for (const rawTarget of branch) {
|
|
352
|
+
if (!rawTarget || typeof rawTarget !== "object" || Array.isArray(rawTarget)) {
|
|
353
|
+
invalidEdges += 1;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const targetRecord = rawTarget;
|
|
357
|
+
if (typeof targetRecord.node !== "string" ||
|
|
358
|
+
(targetRecord.index !== undefined &&
|
|
359
|
+
(!Number.isInteger(targetRecord.index) || targetRecord.index < 0)) ||
|
|
360
|
+
(targetRecord.type !== undefined && typeof targetRecord.type !== "string")) {
|
|
361
|
+
invalidEdges += 1;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const targetIndex = firstIndexByName.get(targetRecord.node);
|
|
365
|
+
if (targetIndex === undefined) {
|
|
366
|
+
danglingTargets += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
edges.push({ sourceIndex, targetIndex });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const errorWorkflow = raw.settings.errorWorkflow;
|
|
375
|
+
const label = optionalLabel(raw.name, input.includeSanitizedLabels);
|
|
376
|
+
return {
|
|
377
|
+
id: input.workflowId,
|
|
378
|
+
...(label ? { label } : {}),
|
|
379
|
+
active: raw.active,
|
|
380
|
+
...(raw.triggerCount !== undefined ? { triggerCount: raw.triggerCount } : {}),
|
|
381
|
+
nodes,
|
|
382
|
+
edges,
|
|
383
|
+
graph: { duplicateNames, invalidEdges, danglingSources, danglingTargets },
|
|
384
|
+
settings: {
|
|
385
|
+
errorWorkflowConfigured: typeof errorWorkflow === "string" && errorWorkflow !== "",
|
|
386
|
+
errorWorkflowSelfReference: errorWorkflow === input.workflowId,
|
|
387
|
+
timezone: timezoneState(raw.settings.timezone),
|
|
388
|
+
saveErrorDataDisabled: raw.settings.saveDataErrorExecution === "none",
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function nodeMaps(nodes, includeLabels) {
|
|
393
|
+
const byName = new Map();
|
|
394
|
+
nodes.forEach((node, index) => {
|
|
395
|
+
if (!byName.has(node.name)) {
|
|
396
|
+
const label = optionalLabel(node.name, includeLabels);
|
|
397
|
+
byName.set(node.name, {
|
|
398
|
+
ref: `node-${index + 1}`,
|
|
399
|
+
...(label === undefined ? {} : { label }),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return byName;
|
|
404
|
+
}
|
|
405
|
+
function reduceDetail(raw, nodes, includeLabels) {
|
|
406
|
+
const references = nodeMaps(nodes, includeLabels);
|
|
407
|
+
const data = raw.data;
|
|
408
|
+
const resultData = data && typeof data.resultData === "object" && data.resultData !== null
|
|
409
|
+
? data.resultData
|
|
410
|
+
: undefined;
|
|
411
|
+
const lastNodeName = typeof resultData?.lastNodeExecuted === "string" ? resultData.lastNodeExecuted : undefined;
|
|
412
|
+
const lastNode = lastNodeName ? references.get(lastNodeName) : undefined;
|
|
413
|
+
const error = resultData?.error;
|
|
414
|
+
const timings = [];
|
|
415
|
+
const runData = resultData && typeof resultData.runData === "object" && resultData.runData !== null
|
|
416
|
+
? resultData.runData
|
|
417
|
+
: undefined;
|
|
418
|
+
if (runData) {
|
|
419
|
+
for (const [nodeName, runs] of Object.entries(runData)) {
|
|
420
|
+
if (!Array.isArray(runs))
|
|
421
|
+
continue;
|
|
422
|
+
const node = references.get(nodeName);
|
|
423
|
+
if (!node)
|
|
424
|
+
continue;
|
|
425
|
+
for (const run of runs) {
|
|
426
|
+
if (!run || typeof run !== "object")
|
|
427
|
+
continue;
|
|
428
|
+
const executionTime = run.executionTime;
|
|
429
|
+
if (typeof executionTime !== "number" ||
|
|
430
|
+
!Number.isFinite(executionTime) ||
|
|
431
|
+
executionTime < 0)
|
|
432
|
+
continue;
|
|
433
|
+
timings.push({
|
|
434
|
+
nodeRef: node.ref,
|
|
435
|
+
...(node.label ? { label: node.label } : {}),
|
|
436
|
+
executionTimeMs: executionTime,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const redactionInfo = data && typeof data.redactionInfo === "object" && data.redactionInfo !== null
|
|
442
|
+
? data.redactionInfo
|
|
443
|
+
: undefined;
|
|
444
|
+
const errorFingerprint = fingerprintError(error, lastNode?.ref);
|
|
445
|
+
return {
|
|
446
|
+
status: normalizeStatus(raw.status),
|
|
447
|
+
...(lastNode ? { lastNodeRef: lastNode.ref } : {}),
|
|
448
|
+
...(lastNode?.label ? { lastNodeLabel: lastNode.label } : {}),
|
|
449
|
+
...(errorFingerprint ? { errorFingerprint } : {}),
|
|
450
|
+
nodeTimings: timings,
|
|
451
|
+
redactionObserved: redactionInfo?.isRedacted === true,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function selectDetails(executions) {
|
|
455
|
+
const newestFirst = [...executions].sort((left, right) => (timestampMs(right.startedAt) ?? 0) - (timestampMs(left.startedAt) ?? 0) ||
|
|
456
|
+
compareCodeUnits(left.id, right.id));
|
|
457
|
+
const errors = newestFirst.filter((execution) => execution.status === "error").slice(0, 3);
|
|
458
|
+
const slowSuccess = newestFirst
|
|
459
|
+
.filter((execution) => execution.status === "success" && executionDuration(execution) !== undefined)
|
|
460
|
+
.sort((left, right) => (executionDuration(right) ?? 0) - (executionDuration(left) ?? 0) ||
|
|
461
|
+
compareCodeUnits(left.id, right.id))[0];
|
|
462
|
+
const selected = slowSuccess ? [...errors, slowSuccess] : errors;
|
|
463
|
+
const seen = new Set();
|
|
464
|
+
return selected
|
|
465
|
+
.filter((execution) => {
|
|
466
|
+
if (seen.has(execution.id))
|
|
467
|
+
return false;
|
|
468
|
+
seen.add(execution.id);
|
|
469
|
+
return true;
|
|
470
|
+
})
|
|
471
|
+
.slice(0, 4);
|
|
472
|
+
}
|
|
473
|
+
export async function collectSnapshot(client, input) {
|
|
474
|
+
const profileBudget = PROFILE_BUDGETS[input.profile];
|
|
475
|
+
const maximumRequests = profileBudget.workflowRequests + profileBudget.listPages + profileBudget.detailRequests;
|
|
476
|
+
const requestTimeoutMs = Math.min(profileBudget.requestTimeoutMs, Math.floor(profileBudget.totalDeadlineMs / maximumRequests));
|
|
477
|
+
let acceptedBytes = 0;
|
|
478
|
+
let pages = 0;
|
|
479
|
+
let detailRequests = 0;
|
|
480
|
+
let status = "complete";
|
|
481
|
+
const limitations = [];
|
|
482
|
+
const read = async (endpoint, query, perResponseBytes) => {
|
|
483
|
+
const remainingBytes = profileBudget.totalBytes - acceptedBytes;
|
|
484
|
+
if (remainingBytes <= 0) {
|
|
485
|
+
throw new IntrospectCollectionError("response_too_large", "The total response budget was exhausted.");
|
|
486
|
+
}
|
|
487
|
+
const result = await client.get(endpoint, query, {
|
|
488
|
+
maxBytes: Math.min(perResponseBytes, remainingBytes),
|
|
489
|
+
timeoutMs: requestTimeoutMs,
|
|
490
|
+
});
|
|
491
|
+
if (result.bytes < 0 || result.bytes > Math.min(perResponseBytes, remainingBytes)) {
|
|
492
|
+
throw new IntrospectCollectionError("response_too_large", "The response reader exceeded its assigned byte limit.");
|
|
493
|
+
}
|
|
494
|
+
acceptedBytes += result.bytes;
|
|
495
|
+
return result;
|
|
496
|
+
};
|
|
497
|
+
const workflowRead = await read(`/workflows/${encodeURIComponent(input.workflowId)}`,
|
|
498
|
+
// Exclude pinned data: Introspect never reads pinData, and including it can push
|
|
499
|
+
// an otherwise small workflow past the bounded byte cap and hard-fail the tool.
|
|
500
|
+
{ excludePinnedData: "true" }, profileBudget.workflowBytes);
|
|
501
|
+
const rawWorkflow = parseOrThrow(WorkflowResponseSchema, workflowRead.value);
|
|
502
|
+
const workflow = reduceWorkflow(rawWorkflow, input);
|
|
503
|
+
const collected = [];
|
|
504
|
+
const seenCursors = new Set();
|
|
505
|
+
let cursor;
|
|
506
|
+
let nextCursor;
|
|
507
|
+
let lookbackReached = false;
|
|
508
|
+
let pageFailure = false;
|
|
509
|
+
let cutoff;
|
|
510
|
+
for (let page = 0; page < profileBudget.listPages && collected.length < input.maxExecutions; page += 1) {
|
|
511
|
+
const remaining = input.maxExecutions - collected.length;
|
|
512
|
+
const query = {
|
|
513
|
+
workflowId: input.workflowId,
|
|
514
|
+
includeData: "false",
|
|
515
|
+
limit: String(Math.min(25, remaining)),
|
|
516
|
+
};
|
|
517
|
+
if (cursor)
|
|
518
|
+
query.cursor = cursor;
|
|
519
|
+
try {
|
|
520
|
+
const pageRead = await read("/executions", query, profileBudget.listBytes);
|
|
521
|
+
const parsed = parseOrThrow(ExecutionListResponseSchema, pageRead.value);
|
|
522
|
+
pages += 1;
|
|
523
|
+
collected.push(...parsed.data.slice(0, remaining).map(reduceExecution));
|
|
524
|
+
nextCursor = parsed.nextCursor;
|
|
525
|
+
const currentOrdering = assessOrdering(collected);
|
|
526
|
+
const validTimes = collected
|
|
527
|
+
.map((execution) => timestampMs(execution.startedAt))
|
|
528
|
+
.filter((value) => value !== undefined);
|
|
529
|
+
if (cutoff === undefined && validTimes.length > 0) {
|
|
530
|
+
cutoff = Math.max(...validTimes) - input.lookbackHours * 60 * 60 * 1_000;
|
|
531
|
+
}
|
|
532
|
+
const activeCutoff = cutoff;
|
|
533
|
+
if (currentOrdering === "verified_newest_first" &&
|
|
534
|
+
activeCutoff !== undefined &&
|
|
535
|
+
validTimes.some((value) => value < activeCutoff)) {
|
|
536
|
+
lookbackReached = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (!nextCursor)
|
|
540
|
+
break;
|
|
541
|
+
if (seenCursors.has(nextCursor)) {
|
|
542
|
+
status = "partial";
|
|
543
|
+
limitations.push(limitation("repeated_cursor"));
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
seenCursors.add(nextCursor);
|
|
547
|
+
cursor = nextCursor;
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
if (page === 0)
|
|
551
|
+
throw error;
|
|
552
|
+
status = "partial";
|
|
553
|
+
pageFailure = true;
|
|
554
|
+
const code = errorCode(error);
|
|
555
|
+
if (code === undefined)
|
|
556
|
+
throw error;
|
|
557
|
+
limitations.push(limitation(code === "upstream_http_error" ? "page_failed" : code));
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const ordering = assessOrdering(collected);
|
|
562
|
+
if (ordering === "unreliable") {
|
|
563
|
+
status = "partial";
|
|
564
|
+
limitations.push(limitation("ordering_unreliable"));
|
|
565
|
+
}
|
|
566
|
+
const invalidTimestampCount = collected.filter((execution) => timestampMs(execution.startedAt) === undefined).length;
|
|
567
|
+
if (invalidTimestampCount > 0) {
|
|
568
|
+
status = "partial";
|
|
569
|
+
limitations.push(limitation("invalid_timestamp"));
|
|
570
|
+
}
|
|
571
|
+
const executionsWithId = collected.filter((execution) => {
|
|
572
|
+
const time = timestampMs(execution.startedAt);
|
|
573
|
+
return time !== undefined && cutoff !== undefined && time >= cutoff;
|
|
574
|
+
});
|
|
575
|
+
const executions = executionsWithId.map(({ id: _id, ...execution }) => execution);
|
|
576
|
+
const validSampleTimes = executions
|
|
577
|
+
.map((execution) => timestampMs(execution.startedAt))
|
|
578
|
+
.filter((value) => value !== undefined);
|
|
579
|
+
let historyBoundary;
|
|
580
|
+
if (ordering === "unreliable")
|
|
581
|
+
historyBoundary = "ordering_unreliable";
|
|
582
|
+
else if (ordering === "unknown" && collected.length > 0)
|
|
583
|
+
historyBoundary = "unknown";
|
|
584
|
+
else if (lookbackReached)
|
|
585
|
+
historyBoundary = "lookback_reached";
|
|
586
|
+
else if (!nextCursor)
|
|
587
|
+
historyBoundary = "complete";
|
|
588
|
+
else if (collected.length >= input.maxExecutions)
|
|
589
|
+
historyBoundary = "execution_limited";
|
|
590
|
+
else if (pageFailure || pages >= profileBudget.listPages)
|
|
591
|
+
historyBoundary = "request_limited";
|
|
592
|
+
else
|
|
593
|
+
historyBoundary = "unknown";
|
|
594
|
+
const details = [];
|
|
595
|
+
if (input.profile === "deep") {
|
|
596
|
+
for (const execution of selectDetails(executionsWithId)) {
|
|
597
|
+
if (detailRequests >= profileBudget.detailRequests)
|
|
598
|
+
break;
|
|
599
|
+
if (!safeExecutionId(execution.id)) {
|
|
600
|
+
status = "partial";
|
|
601
|
+
limitations.push(limitation("invalid_execution_id"));
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
detailRequests += 1;
|
|
605
|
+
try {
|
|
606
|
+
const detailRead = await read(`/executions/${encodeURIComponent(execution.id)}`, {
|
|
607
|
+
includeData: "true",
|
|
608
|
+
redactExecutionData: "true",
|
|
609
|
+
ignoreDataSizeLimit: "false",
|
|
610
|
+
}, profileBudget.detailBytes);
|
|
611
|
+
const parsed = parseOrThrow(ExecutionDetailResponseSchema, detailRead.value);
|
|
612
|
+
details.push(reduceDetail(parsed, rawWorkflow.nodes, input.includeSanitizedLabels));
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
status = "partial";
|
|
616
|
+
const code = errorCode(error);
|
|
617
|
+
if (code === undefined)
|
|
618
|
+
throw error;
|
|
619
|
+
limitations.push(limitation(code === "upstream_http_error" ? "detail_failed" : code));
|
|
620
|
+
if (code === "deadline_exceeded" || code === "response_too_large")
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
input,
|
|
627
|
+
workflow,
|
|
628
|
+
executions,
|
|
629
|
+
details,
|
|
630
|
+
status,
|
|
631
|
+
pages,
|
|
632
|
+
detailRequests,
|
|
633
|
+
acceptedBytes,
|
|
634
|
+
ordering,
|
|
635
|
+
historyBoundary,
|
|
636
|
+
oldestStartedAt: validSampleTimes.length > 0 ? new Date(Math.min(...validSampleTimes)).toISOString() : null,
|
|
637
|
+
newestStartedAt: validSampleTimes.length > 0 ? new Date(Math.max(...validSampleTimes)).toISOString() : null,
|
|
638
|
+
totalMetadataExecutions: collected.length,
|
|
639
|
+
sampledErrors: executions.filter((execution) => execution.status === "error").length,
|
|
640
|
+
limitations,
|
|
641
|
+
};
|
|
642
|
+
}
|