bpmnlint-plugin-camunda-compat 2.60.0 → 2.60.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpmnlint-plugin-camunda-compat",
3
- "version": "2.60.0",
3
+ "version": "2.60.2",
4
4
  "description": "A bpmnlint plug-in for Camunda compatibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -15,10 +15,11 @@ const { annotateRule } = require('../helper');
15
15
  * or a part like `toolCallResult.statusCode`), a script task or called decision
16
16
  * `resultVariable`, or a connector `resultVariable`/`resultExpression` header.
17
17
  *
18
- * When the tool flow writes result-shaped variables but none of them is
19
- * `toolCallResult`, every miswrite is reported individually on the element
20
- * that wrote it (misdirection or wrong casing), so each offending output row
21
- * carries its own finding. When the flow writes none at all, the rule reports
18
+ * A wrong-casing near-miss (`toolcallresult`, `TOOLCALLRESULT`, ...) is always
19
+ * reported on the element that wrote it, even when a sibling write already
20
+ * sets `toolCallResult` correctly case-sensitive engines never read it.
21
+ * Other miswrites are reported individually per row only while no valid
22
+ * `toolCallResult` exists. When the flow writes none at all, the rule reports
22
23
  * once on the entry activity, since there's no single offending element to
23
24
  * point to (the agent gets no completion signal and may retry or hallucinate
24
25
  * an outcome). Results written from arbitrary FEEL expressions are not
@@ -70,6 +71,19 @@ module.exports = skipInNonExecutableProcess(function(config = {}) {
70
71
  return;
71
72
  }
72
73
 
74
+ // Miscased near-misses are reported even when a sibling already sets
75
+ // toolCallResult correctly. Casing variants never satisfy
76
+ // isToolCallResultChannel, so the overwrite check below ignores them.
77
+ for (const channel of channels) {
78
+ if (isToolCallResultCasingMismatch(channel)) {
79
+ reportErrors(channel.element, reporter, {
80
+ message: `Wrong casing "${getCasingMismatchText(channel)}": use toolCallResult (case-sensitive).`,
81
+ data: { type: ERROR_TYPES.AGENT_TOOL_OUTPUT_KEY_CASING_INVALID },
82
+ path: getChannelPath(channel),
83
+ });
84
+ }
85
+ }
86
+
73
87
  // toolCallResult is set somewhere, but assigning it more than once
74
88
  // overwrites the earlier value. Part contributions (toolCallResult.part)
75
89
  // and the context put(toolCallResult, ...) accumulation pattern are
@@ -267,7 +281,7 @@ function isToolCallResultChannel({ kind, value }) {
267
281
  // case-insensitive name matching for fromAi().
268
282
  function isToolCallResultCasingMismatch({ kind, value }) {
269
283
  if (kind === 'resultExpression') {
270
- return /\btoolcallresult\b/i.test(value) && !/\btoolCallResult\b/.test(value);
284
+ return !!getResultExpressionCasingMismatch(value);
271
285
  }
272
286
 
273
287
  const lower = value.toLowerCase();
@@ -276,13 +290,21 @@ function isToolCallResultCasingMismatch({ kind, value }) {
276
290
  return isCasingVariant && !isToolCallResultChannel({ kind, value });
277
291
  }
278
292
 
293
+ // The first miscased toolCallResult token in a FEEL expression, or null when
294
+ // every occurrence is cased correctly. Tokens are judged individually, so a
295
+ // correct occurrence never excuses a miscased sibling in the same expression.
296
+ function getResultExpressionCasingMismatch(value) {
297
+ const tokens = value.match(/\btoolcallresult\b/gi) || [];
298
+
299
+ return tokens.find(token => token !== 'toolCallResult') || null;
300
+ }
301
+
279
302
  // The mismatched word itself, not the whole expression, for resultExpression
280
303
  // channels (a connector header value is a full FEEL expression, e.g.
281
304
  // `={toolcallresult: response.body}`).
282
305
  function getCasingMismatchText({ kind, value }) {
283
306
  if (kind === 'resultExpression') {
284
- const match = value.match(/\btoolcallresult\b/i);
285
- return match ? match[0] : value;
307
+ return getResultExpressionCasingMismatch(value) || value;
286
308
  }
287
309
 
288
310
  return value;
@@ -15,7 +15,13 @@ const { reportErrors } = require('../utils/reporter');
15
15
 
16
16
  const { skipInNonExecutableProcess } = require('../utils/rule');
17
17
 
18
- module.exports = skipInNonExecutableProcess(function() {
18
+ const { greaterOrEqual } = require('../utils/version');
19
+
20
+ // `camunda.secrets.<name>` only resolves on engines with this version or newer;
21
+ // before that, `{{secrets.<name>}}` remains the only working format
22
+ const CAMUNDA_SECRETS_FORMAT_ALLOWED_VERSION = '8.10';
23
+
24
+ module.exports = skipInNonExecutableProcess(function({ version }) {
19
25
  function check(node, reporter) {
20
26
  const errors = [
21
27
  validateIoMapping,
@@ -33,66 +39,85 @@ module.exports = skipInNonExecutableProcess(function() {
33
39
  }
34
40
  }
35
41
 
36
- return {
37
- check
38
- };
39
- });
42
+ function validateIoMapping(node) {
43
+ const ioMapping = findExtensionElement(node, 'zeebe:IoMapping');
40
44
 
41
- function validateIoMapping(node) {
42
- const ioMapping = findExtensionElement(node, 'zeebe:IoMapping');
45
+ if (!ioMapping) {
46
+ return [];
47
+ }
43
48
 
44
- if (!ioMapping) {
45
- return [];
49
+ return ioMapping.get('inputParameters')
50
+ .filter(inputParameter => !isValidSecret(inputParameter.get('source')))
51
+ .map(inputParameter => getReport('source', inputParameter, node));
46
52
  }
47
53
 
48
- return ioMapping.get('inputParameters')
49
- .filter(inputParameter => !isValidSecret(inputParameter.get('source')))
50
- .map(inputParameter => getReport('source', inputParameter, node));
51
- }
54
+ function validateProperties(node) {
55
+ const properties = findExtensionElement(node, 'zeebe:Properties');
52
56
 
53
- function validateProperties(node) {
54
- const properties = findExtensionElement(node, 'zeebe:Properties');
57
+ if (!properties) {
58
+ return [];
59
+ }
55
60
 
56
- if (!properties) {
57
- return [];
61
+ return (properties.get('properties'))
62
+ .filter(property => !isValidSecret(property.get('value')))
63
+ .map(property => getReport('value', property, node));
58
64
  }
59
65
 
60
- return (properties.get('properties'))
61
- .filter(property => !isValidSecret(property.get('value')))
62
- .map(property => getReport('value', property, node));
63
- }
66
+ function validateSubscription(node) {
67
+ let message;
64
68
 
65
- function validateSubscription(node) {
66
- let message;
69
+ if (is(node, 'bpmn:ReceiveTask')) {
70
+ message = node.get('messageRef');
71
+ } else {
72
+ const messageEventDefinition = getEventDefinition(node, 'bpmn:MessageEventDefinition');
67
73
 
68
- if (is(node, 'bpmn:ReceiveTask')) {
69
- message = node.get('messageRef');
70
- } else {
71
- const messageEventDefinition = getEventDefinition(node, 'bpmn:MessageEventDefinition');
74
+ if (!messageEventDefinition) {
75
+ return [];
76
+ }
72
77
 
73
- if (!messageEventDefinition) {
78
+ message = messageEventDefinition.get('messageRef');
79
+ }
80
+
81
+ if (!message) {
74
82
  return [];
75
83
  }
76
84
 
77
- message = messageEventDefinition.get('messageRef');
78
- }
85
+ const subscription = findExtensionElement(message, 'zeebe:Subscription');
79
86
 
80
- if (!message) {
81
- return [];
82
- }
87
+ if (!subscription) {
88
+ return [];
89
+ }
83
90
 
84
- const subscription = findExtensionElement(message, 'zeebe:Subscription');
91
+ const correlationKey = subscription.get('correlationKey');
85
92
 
86
- if (!subscription) {
87
- return [];
93
+ return isValidSecret(correlationKey)
94
+ ? []
95
+ : [ getReport('correlationKey', subscription, node) ];
88
96
  }
89
97
 
90
- const correlationKey = subscription.get('correlationKey');
98
+ function isValidSecret(value) {
99
+ if (!value || !isString(value) || !value.includes('secrets.')) {
100
+ return true;
101
+ }
91
102
 
92
- return isValidSecret(correlationKey)
93
- ? []
94
- : [ getReport('correlationKey', subscription, node) ];
95
- }
103
+ // the current, opt-in `camunda.secrets.<name>` format is always accepted
104
+ if (/camunda\.secrets\.[\w-]+/.test(value)) {
105
+ return true;
106
+ }
107
+
108
+ // once the engine supports `camunda.secrets.<name>`, any remaining
109
+ // `secrets.<name>` reference (wrapped or not) is considered outdated
110
+ if (greaterOrEqual(version, CAMUNDA_SECRETS_FORMAT_ALLOWED_VERSION)) {
111
+ return false;
112
+ }
113
+
114
+ return /{{\s*secrets\.[\w-]+\s*}}/.test(value);
115
+ }
116
+
117
+ return {
118
+ check
119
+ };
120
+ });
96
121
 
97
122
  function getReport(propertyName, node, parentNode) {
98
123
  const path = getPath(node, parentNode);
@@ -104,14 +129,8 @@ function getReport(propertyName, node, parentNode) {
104
129
  type: ERROR_TYPES.SECRET_EXPRESSION_FORMAT_DEPRECATED,
105
130
  node,
106
131
  parentNode: parentNode,
107
- property: propertyName
132
+ property: propertyName,
133
+ allowedVersion: CAMUNDA_SECRETS_FORMAT_ALLOWED_VERSION
108
134
  }
109
135
  };
110
136
  }
111
-
112
- function isValidSecret(value) {
113
- return !value
114
- || !isString(value)
115
- || !value.includes('secrets.')
116
- || /{{\s*secrets\.[\w-]+\s*}}/.test(value);
117
- }