@xano/xanoscript-language-server 12.16.6 → 12.18.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.
Files changed (40) hide show
  1. package/cache/documentCache.js +12 -5
  2. package/language-server-worker.js +6 -0
  3. package/lexer/mcp.js +109 -0
  4. package/lexer/policy.js +30 -0
  5. package/lexer/tokens.js +14 -0
  6. package/onCompletion/contentAssist.js +6 -0
  7. package/onCompletion/onCompletion.js +33 -3
  8. package/onCompletion/policyCompletion.js +339 -0
  9. package/onHover/PolicyMessageProvider.js +142 -0
  10. package/onHover/functions.md +39 -0
  11. package/onHover/onHover.js +2 -0
  12. package/package.json +3 -1
  13. package/parser/base_parser.js +2 -0
  14. package/parser/clauses/inputClause.js +13 -4
  15. package/parser/clauses/mcpMetadataClauses.js +252 -0
  16. package/parser/clauses/nakedStackFn.js +8 -0
  17. package/parser/clauses/register.js +16 -0
  18. package/parser/definitions/columnDefinition.js +1 -0
  19. package/parser/definitions/enumColumnDefinition.js +1 -0
  20. package/parser/definitions/objectColumnDefinition.js +1 -0
  21. package/parser/functions/mcp/elicitLint.js +100 -0
  22. package/parser/functions/mcp/mcpElicitFn.js +103 -0
  23. package/parser/functions/mcp/mcpProgressFn.js +34 -0
  24. package/parser/functions/mcp/register.js +11 -0
  25. package/parser/functions/mcpFn.js +19 -0
  26. package/parser/functions/register.js +4 -0
  27. package/parser/mcp_primitive_parser.js +343 -0
  28. package/parser/mcp_server_parser.js +53 -0
  29. package/parser/multidoc.js +45 -13
  30. package/parser/parser.js +58 -0
  31. package/parser/policy/catalogue.js +100 -0
  32. package/parser/policy/catalogueSnapshot.js +7075 -0
  33. package/parser/policy/policyDocs.js +76 -0
  34. package/parser/policy/policyLiteral.js +227 -0
  35. package/parser/policy/validatePolicyRule.js +323 -0
  36. package/parser/policy_parser.js +463 -0
  37. package/parser/register.js +21 -0
  38. package/parser/tool_parser.js +32 -0
  39. package/server.js +6 -0
  40. package/utils.js +6 -2
@@ -0,0 +1,463 @@
1
+ import { CommentToken } from "../lexer/comment.js";
2
+ import { EqualToken, LCurly, LSquare, Minus, RCurly } from "../lexer/control.js";
3
+ import {
4
+ FloatLiteral,
5
+ IntegerLiteral,
6
+ SingleQuotedStringLiteral,
7
+ StringLiteral,
8
+ } from "../lexer/literal.js";
9
+ import { PolicyToken } from "../lexer/policy.js";
10
+ import { DotToken, Identifier, NewlineToken } from "../lexer/tokens.js";
11
+ import { getPolicyCatalogue } from "./policy/catalogue.js";
12
+ import { policyStringValue, readPolicyLiteral } from "./policy/policyLiteral.js";
13
+ import { didYouMean, validatePolicyRule } from "./policy/validatePolicyRule.js";
14
+
15
+ // Grammar for a `policy <key> { ... }` document.
16
+ //
17
+ // The language server knows the block SHAPE, as it knows every other object type's:
18
+ //
19
+ // policy <key> {
20
+ // <field> = <literal> # one per line
21
+ // rule { <field> = <literal> } # repeating; `check` names a catalogue item, `params` its params
22
+ // }
23
+ //
24
+ // with the comment, named-rule and literal-value rules of cloud-client's Script.php. It knows no
25
+ // field: which fields a policy and a rule may set, their types, value sets and defaults, the retired
26
+ // fields and reserved blocks, the key pattern and when a rule is required all come from the
27
+ // catalogue's `document` (helper/mvp/policy/Document.php), and every check from its `items`. With no
28
+ // catalogue loaded, only the shape is checked. See parser/policy/catalogue.js.
29
+
30
+ /** The platform's refusal of a named rule, after its `rule[<i>]: ` prefix. */
31
+ const RULE_NAMED =
32
+ 'A rule cannot be named. Write "rule {" — rules are identified by position (KEY.R1, KEY.R2…).';
33
+
34
+ /** The platform's refusal of a comment inside a policy block, naming the comment's line. */
35
+ const COMMENT_REFUSED = (kind, line) =>
36
+ `line ${line}: policy files cannot contain "${kind}" comments. Put the explanation in the policy's statement, rationale or narrative.`;
37
+
38
+ /** `sprintf` for the platform's `%s` / `%d` sentences; the value is inserted as it is. */
39
+ const fill = (message, value) => String(message).replace(/%[sd]/, () => String(value));
40
+
41
+ /**
42
+ * `<key>` after `policy`: a quoted string, or letters, digits, ".", "_" and "-" written together,
43
+ * which the lexer splits into several tokens (`AUTH-001` is `AUTH`, `-`, `001`).
44
+ * @param {import('./base_parser.js').XanoBaseParser} $
45
+ */
46
+ export function policyKeyFn($) {
47
+ return (tokens) => {
48
+ const add = (token) => $.ACTION(() => tokens.push(token));
49
+ $.OR([
50
+ { ALT: () => add($.CONSUME(StringLiteral)) },
51
+ { ALT: () => add($.CONSUME(SingleQuotedStringLiteral)) },
52
+ {
53
+ ALT: () => {
54
+ $.AT_LEAST_ONE(() => {
55
+ add(
56
+ $.OR1([
57
+ { ALT: () => $.CONSUME(Identifier) },
58
+ { ALT: () => $.CONSUME(IntegerLiteral) },
59
+ { ALT: () => $.CONSUME(FloatLiteral) },
60
+ { ALT: () => $.CONSUME(Minus) },
61
+ { ALT: () => $.CONSUME(DotToken) },
62
+ ])
63
+ );
64
+ });
65
+ },
66
+ },
67
+ ]);
68
+ };
69
+ }
70
+
71
+ /**
72
+ * `name = value`, in a policy or a rule. Any value expression parses here; what the field may hold
73
+ * (a literal of its declared type) is checked once the block is read.
74
+ * @param {import('./base_parser.js').XanoBaseParser} $
75
+ */
76
+ export function policyFieldFn($) {
77
+ return (fields) => {
78
+ const keyToken = $.CONSUME(Identifier);
79
+ $.CONSUME(EqualToken); // "="
80
+ const first = $.ACTION(() => $.LA(1));
81
+ $.SUBRULE($.schemaFn, { ARGS: [keyToken, "[expression]"] });
82
+ $.ACTION(() => {
83
+ fields.push({ name: keyToken.image, keyToken, first, last: $.LA(0) });
84
+ const next = $.LA(1);
85
+ if (next.tokenType !== NewlineToken && next.image !== "}" && next.startLine === $.LA(0).endLine) {
86
+ $.addInvalidValueError(next, "Policy fields must be separated by a newline.");
87
+ }
88
+ });
89
+ };
90
+ }
91
+
92
+ /**
93
+ * `{ ... }` of a rule (or of a block that is not one, read the same way so the rest parses): fields
94
+ * only, one per line.
95
+ * @param {import('./base_parser.js').XanoBaseParser} $
96
+ */
97
+ export function policyBlockBodyFn($) {
98
+ return (block) => {
99
+ $.CONSUME(LCurly); // "{"
100
+ // Native blocks also allow compact, same-line fields.
101
+ $.MANY(() => {
102
+ $.OR([
103
+ { ALT: () => $.CONSUME(NewlineToken) },
104
+ {
105
+ ALT: () => {
106
+ const comment = $.CONSUME(CommentToken);
107
+ $.ACTION(() => block.comments.push(comment));
108
+ },
109
+ },
110
+ { ALT: () => $.SUBRULE($.policyFieldFn, { ARGS: [block?.fields] }) },
111
+ ]);
112
+ });
113
+ $.CONSUME(RCurly); // "}"
114
+ };
115
+ }
116
+
117
+ /**
118
+ * `rule { ... }`, and anything else written as `<word> [name] { ... }` inside a policy (`guard`,
119
+ * `test`, a misplaced `remediation { }`): read alike, told apart when the policy is checked. A name
120
+ * is read the way a key is (a quoted string, or a bare run such as `1`, `a.b` or `x-2`), since the
121
+ * platform refuses each of them as a name.
122
+ * @param {import('./base_parser.js').XanoBaseParser} $
123
+ */
124
+ export function policyBlockFn($) {
125
+ return (blocks) => {
126
+ const block = { token: null, nameToken: null, fields: [], comments: [] };
127
+ const nameTokens = [];
128
+ block.token = $.CONSUME(Identifier); // "rule"
129
+ $.OPTION(() => $.SUBRULE($.policyKeyFn, { ARGS: [nameTokens] }));
130
+ $.ACTION(() => {
131
+ block.nameToken = nameTokens[0] ?? null;
132
+ });
133
+ $.SUBRULE($.policyBlockBodyFn, { ARGS: [block] });
134
+ $.ACTION(() => blocks.push(block));
135
+ };
136
+ }
137
+
138
+ /**
139
+ * @param {import('./base_parser.js').XanoBaseParser} $
140
+ */
141
+ export function policyDeclaration($) {
142
+ return () => {
143
+ const fields = [];
144
+ const blocks = [];
145
+ const comments = [];
146
+
147
+ $.sectionStack.push("policyDeclaration");
148
+ // Comments before the block are formatting; inside it they are refused.
149
+ $.MANY(() => $.OR([
150
+ { ALT: () => $.CONSUME(NewlineToken) },
151
+ { ALT: () => $.CONSUME(CommentToken) },
152
+ ]));
153
+ const policyToken = $.CONSUME(PolicyToken); // "policy"
154
+ const keyTokens = [];
155
+ $.SUBRULE($.policyKeyFn, { ARGS: [keyTokens] });
156
+ $.CONSUME(LCurly); // "{"
157
+ $.MANY1(() => {
158
+ $.OR1([
159
+ { ALT: () => $.CONSUME1(NewlineToken) },
160
+ {
161
+ ALT: () => {
162
+ const comment = $.CONSUME1(CommentToken);
163
+ $.ACTION(() => comments.push(comment));
164
+ },
165
+ },
166
+ { ALT: () => $.SUBRULE($.policyFieldFn, { ARGS: [fields] }) },
167
+ { ALT: () => $.SUBRULE($.policyBlockFn, { ARGS: [blocks] }) },
168
+ ]);
169
+ });
170
+ $.CONSUME(RCurly); // "}"
171
+ $.MANY2(() => $.OR2([
172
+ { ALT: () => $.CONSUME2(NewlineToken) },
173
+ { ALT: () => $.CONSUME2(CommentToken) },
174
+ ]));
175
+
176
+ $.ACTION(() =>
177
+ checkPolicy($, { policyToken, keyTokens, fields, blocks, comments })
178
+ );
179
+ $.sectionStack.pop();
180
+ };
181
+ }
182
+
183
+ /**
184
+ * The comment starting at `tokens[i]`: an own-line `//` (a comment token), or a `//` or `/*` the lexer
185
+ * read as divisions because something precedes it on the line. With `slashOnly`, only the latter.
186
+ */
187
+ function commentAt(tokens, i, slashOnly = false) {
188
+ const token = tokens[i];
189
+ if (!token) return null;
190
+ if (token.tokenType === CommentToken) return slashOnly ? null : { token, kind: "//" };
191
+ const next = tokens[i + 1];
192
+ if (token.image === "/" && next && token.endOffset + 1 === next.startOffset && (next.image === "/" || next.image === "*")) {
193
+ return { token, kind: next.image === "/" ? "//" : "/* */" };
194
+ }
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * A comment the grammar cannot place (inside `params` or a list, after a value on the same line, or
200
+ * `/* *\/`) stops the parse; say what the platform says about it, at the comment, instead of
201
+ * "Expecting }".
202
+ * @param {import('./base_parser.js').XanoBaseParser} parser after `policyDeclaration()`
203
+ */
204
+ export function explainPolicySyntaxErrors(parser) {
205
+ const tokens = parser.input;
206
+ for (const error of parser.errors) {
207
+ let at = tokens.indexOf(error.token);
208
+ if (at < 0) continue;
209
+ // A `{` or `[` the parse could not read past: a comment may be the first thing inside it.
210
+ if (tokens[at].tokenType === LCurly || tokens[at].tokenType === LSquare) {
211
+ let next = at + 1;
212
+ while (tokens[next]?.tokenType === NewlineToken) next++;
213
+ if (commentAt(tokens, next)) at = next;
214
+ }
215
+ // The parse stops at the comment, or at its second character after reading a `/` as a division.
216
+ const comment = commentAt(tokens, at) ?? commentAt(tokens, at - 1, true);
217
+ if (comment) {
218
+ error.token = comment.token;
219
+ error.message = COMMENT_REFUSED(comment.kind, comment.token.startLine);
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Report through the parser. What the block's shape decides is an error. What the catalogue decides
226
+ * is an error against the instance's own (live) catalogue, and a dated warning against the snapshot
227
+ * shipped with the language server, which may be older than the instance; a finding that mirrors
228
+ * platform logic beyond the published schema (`advisory`) is a warning either way.
229
+ */
230
+ function reporter($, catalogue) {
231
+ const error = (token, message) => $.addInvalidValueError(token, message);
232
+ const warning = (token, message) => $.addWarning(message, token);
233
+ const dated = catalogue.generatedAt ? ` of ${catalogue.generatedAt}` : "";
234
+ return {
235
+ error,
236
+ catalogue: (token, message, advisory = false) => {
237
+ if (catalogue.source === "live" && !advisory) {
238
+ error(token, message);
239
+ } else if (catalogue.source === "snapshot") {
240
+ warning(token, `${message} (checked against the language server's check catalogue${dated}; your instance decides on save)`);
241
+ } else {
242
+ warning(token, message);
243
+ }
244
+ },
245
+ };
246
+ }
247
+
248
+ /** A field's value as literals with their tokens, or null when it has none. */
249
+ function valueOf(input, field) {
250
+ if (!field?.first || !field?.last) return null;
251
+ return readPolicyLiteral(input, input.indexOf(field.first), input.indexOf(field.last));
252
+ }
253
+
254
+ /**
255
+ * Whether a field's literal value has the type the document declares (`string`, `string[]`,
256
+ * `bool`, `object`) and one of its `values`; the sentence when not.
257
+ */
258
+ function fieldProblem(name, node, spec) {
259
+ const type = spec?.type ?? "string";
260
+ if (type === "string[]") {
261
+ if (node.kind !== "array" || node.items.some((item) => item?.kind !== "string")) {
262
+ return { token: node.token, message: `${name} must be a list of strings.` };
263
+ }
264
+ const max = spec.max_length;
265
+ const long = max ? node.items.findIndex((item) => [...item.value.trim()].length > max) : -1;
266
+ if (long >= 0) {
267
+ return { token: node.items[long].token, message: `${name}[${long}] must be ${max} characters or fewer.` };
268
+ }
269
+ return null;
270
+ }
271
+ if (type === "bool") {
272
+ return node.kind === "boolean" ? null : { token: node.token, message: `${name} must be true or false.` };
273
+ }
274
+ if (type === "object") {
275
+ return node.kind === "object" ? null : { token: node.token, message: `${name} must be an object.` };
276
+ }
277
+ if (node.kind !== "string") {
278
+ return { token: node.token, message: `${name} must be a string.` };
279
+ }
280
+ if (spec?.values?.length && !spec.values.includes(node.value.trim())) {
281
+ return {
282
+ token: node.token,
283
+ message: `${name} must be one of: ${spec.values.join(", ")} (got "${node.value.trim()}").`,
284
+ };
285
+ }
286
+ return null;
287
+ }
288
+
289
+ /**
290
+ * A rule's params: an object of literal values (Script::literal; a name written twice is already
291
+ * reported by the object grammar). The catalogue then says what the object may hold.
292
+ */
293
+ function checkParamsShape(node, report) {
294
+ if (!node) return;
295
+ // The native transform normalizes null and an empty list to empty params.
296
+ if (node.kind === "null" || (node.kind === "array" && node.items.length === 0)) return;
297
+ if (node.kind !== "object") {
298
+ report.error(
299
+ node.token,
300
+ node.kind === "expression"
301
+ ? "Policy parameters must be literal values."
302
+ : 'params must be an object, e.g. params = { tags: ["pci"] }.'
303
+ );
304
+ return;
305
+ }
306
+ const visit = (value) => {
307
+ if (!value) return;
308
+ if (value.kind === "expression") {
309
+ report.error(value.token, "Policy parameters must be literal values.");
310
+ } else if (value.kind === "object") {
311
+ value.entries.forEach((entry) => visit(entry.value));
312
+ } else if (value.kind === "array") {
313
+ value.items.forEach(visit);
314
+ }
315
+ };
316
+ visit(node);
317
+ }
318
+
319
+ /**
320
+ * One block's fields (a policy's or a rule's): each once, a literal, and, when the catalogue
321
+ * describes the document, one it declares, of its type. `retired` and `where` shape the sentences.
322
+ * @returns {Object<string, {field: Object, node: Object|null}>} the fields by name
323
+ */
324
+ function checkFields(fields, { input, report, specs, retired, where, extra = [], index }) {
325
+ const out = Object.create(null);
326
+ const names = specs ? [...Object.keys(specs), ...extra] : [];
327
+ for (const field of fields) {
328
+ const { name, keyToken } = field;
329
+ if (retired && Object.hasOwn(retired, name)) {
330
+ report.catalogue(keyToken, fill(retired[name], index));
331
+ continue;
332
+ }
333
+ if (specs && !Object.hasOwn(specs, name)) {
334
+ report.catalogue(
335
+ keyToken,
336
+ `${where}: unknown field "${name}". ${didYouMean(name, names)}${where === "policy" ? "Policy" : "Rule"} fields: ${names.join(", ")}.`
337
+ );
338
+ continue;
339
+ }
340
+ if (name in out) {
341
+ report.error(keyToken, `${where}: "${name}" is set more than once.`);
342
+ continue;
343
+ }
344
+ const node = valueOf(input, field);
345
+ out[name] = { field, node };
346
+ // `params` is the check's own object; checkParamsShape and the catalogue read it.
347
+ if (!node || name === "params") continue;
348
+ if (node.kind === "expression") {
349
+ report.error(node.token, `${name} must be a literal value.`);
350
+ continue;
351
+ }
352
+ const problem = specs ? fieldProblem(name, node, specs[name]) : null;
353
+ if (problem) report.catalogue(problem.token, problem.message);
354
+ }
355
+ return out;
356
+ }
357
+
358
+ /**
359
+ * Everything the platform refuses that the block decides, then each rule against the catalogue.
360
+ */
361
+ function checkPolicy($, { policyToken, keyTokens, fields, blocks, comments }) {
362
+ const catalogue = getPolicyCatalogue();
363
+ const document = catalogue.document;
364
+ const report = reporter($, catalogue);
365
+ const input = $.input;
366
+
367
+ for (const comment of comments) report.error(comment, COMMENT_REFUSED("//", comment.startLine));
368
+
369
+ // The key, as the platform reads it: the text from its first token to its last.
370
+ const keyToken = keyTokens[0] ?? policyToken;
371
+ const contiguous = keyTokens.every(
372
+ (token, i) => i === 0 || keyTokens[i - 1].endOffset + 1 === token.startOffset
373
+ );
374
+ const key =
375
+ keyTokens.length === 1 && /^["']/.test(keyTokens[0].image)
376
+ ? policyStringValue(keyTokens[0].image).trim()
377
+ : contiguous
378
+ ? keyTokens.map((token) => token.image).join("")
379
+ : `${keyTokens[0].image}…${keyTokens[keyTokens.length - 1].image}`;
380
+ if (document?.key?.pattern) {
381
+ let pattern = null;
382
+ try {
383
+ pattern = new RegExp(document.key.pattern);
384
+ } catch {
385
+ pattern = null;
386
+ }
387
+ if (pattern && !pattern.test(key)) report.catalogue(keyToken, fill(document.key.message, key));
388
+ } else if (!contiguous) {
389
+ report.error(keyToken, `Policy key "${key}" must be written without spaces.`);
390
+ }
391
+
392
+ const values = checkFields(fields, {
393
+ input,
394
+ report,
395
+ specs: document?.fields,
396
+ retired: document?.retired?.policy,
397
+ where: "policy",
398
+ extra: ["rule"],
399
+ });
400
+
401
+ // Blocks: rules, and blocks a policy does not have.
402
+ const reserved = document?.reserved_blocks;
403
+ let ruleIndex = 0;
404
+ for (const block of blocks) {
405
+ const word = block.token.image;
406
+ for (const comment of block.comments) report.error(comment, COMMENT_REFUSED("//", comment.startLine));
407
+
408
+ if (word !== "rule") {
409
+ if (reserved?.names?.includes(word)) {
410
+ report.catalogue(block.token, fill(reserved.message, word));
411
+ } else if (document?.retired?.policy && Object.hasOwn(document.retired.policy, word)) {
412
+ report.catalogue(block.token, document.retired.policy[word]);
413
+ } else {
414
+ report.error(block.token, `policy: unknown block "${word}". ${didYouMean(word, ["rule"])}Policies support rule blocks only.`);
415
+ }
416
+ continue;
417
+ }
418
+
419
+ const index = ruleIndex++;
420
+ if (block.nameToken) report.error(block.nameToken, `rule[${index}]: ${RULE_NAMED}`);
421
+
422
+ const rule = checkFields(block.fields, {
423
+ input,
424
+ report,
425
+ specs: document?.rule,
426
+ retired: document?.retired?.rule,
427
+ where: `rule[${index}]`,
428
+ index,
429
+ });
430
+
431
+ checkParamsShape(rule.params?.node, report);
432
+ if (!rule.check) {
433
+ if (document?.rule?.check?.required) {
434
+ report.catalogue(block.token, `rule[${index}]: "check" is required. Name a check from the catalogue: check = "<check id>".`);
435
+ }
436
+ continue;
437
+ }
438
+
439
+ const checkNode = rule.check.node;
440
+ const problems = validatePolicyRule(
441
+ {
442
+ checkId: checkNode?.kind === "string" ? checkNode.value.trim() : null,
443
+ checkToken: checkNode?.token ?? null,
444
+ paramsToken: rule.params?.field.keyToken ?? null,
445
+ params: rule.params?.node ?? null,
446
+ ruleToken: block.token,
447
+ },
448
+ catalogue
449
+ );
450
+ for (const problem of problems) report.catalogue(problem.token, problem.message, problem.advisory);
451
+ }
452
+
453
+ // A policy with no rule: allowed only for the field values the document names.
454
+ const required = document?.rules_required;
455
+ if (required && ruleIndex === 0) {
456
+ const allowed = Object.entries(required.unless ?? {}).some(([name, value]) => {
457
+ const node = values[name]?.node;
458
+ const inForce = node && node.kind !== "expression" ? node.value : document.fields?.[name]?.default;
459
+ return inForce === value;
460
+ });
461
+ if (!allowed) report.catalogue(keyToken, required.message);
462
+ }
463
+ }
@@ -12,6 +12,10 @@ import { errorTriggerDeclaration } from "./error_trigger_parser.js";
12
12
  import { functionDeclaration } from "./function_parser.js";
13
13
  import { register as register_functions } from "./functions/register.js";
14
14
  import { register as register_generic } from "./generic/register.js";
15
+ import {
16
+ promptDeclaration,
17
+ resourceDeclaration,
18
+ } from "./mcp_primitive_parser.js";
15
19
  import { mcpServerDeclaration } from "./mcp_server_parser.js";
16
20
  import { mcpServerTriggerDeclaration } from "./mcp_server_trigger_parser.js";
17
21
  import { messageDeclaration } from "./message_parser.js";
@@ -27,6 +31,13 @@ import {
27
31
  microserviceVolumeClause,
28
32
  } from "./microservice_parser.js";
29
33
  import { middlewareDeclaration } from "./middleware_parser.js";
34
+ import {
35
+ policyBlockBodyFn,
36
+ policyBlockFn,
37
+ policyDeclaration,
38
+ policyFieldFn,
39
+ policyKeyFn,
40
+ } from "./policy_parser.js";
30
41
  import { queryDeclaration } from "./query_parser.js";
31
42
  import { realtimeChannelDeclaration } from "./realtime_channel_parser.js";
32
43
  import { realtimeServerDeclaration } from "./realtime_server_parser.js";
@@ -63,7 +74,12 @@ export const register = ($) => {
63
74
  "agentTriggerDeclaration",
64
75
  agentTriggerDeclaration($)
65
76
  );
77
+ $.promptDeclaration = $.RULE("promptDeclaration", promptDeclaration($));
66
78
  $.queryDeclaration = $.RULE("queryDeclaration", queryDeclaration($));
79
+ $.resourceDeclaration = $.RULE(
80
+ "resourceDeclaration",
81
+ resourceDeclaration($)
82
+ );
67
83
  $.apiGroupDeclaration = $.RULE("apiGroupDeclaration", apiGroupDeclaration($));
68
84
  $.branchDeclaration = $.RULE("branchDeclaration", branchDeclaration($));
69
85
  $.channelDeclaration = $.RULE("channelDeclaration", channelDeclaration($));
@@ -117,6 +133,11 @@ export const register = ($) => {
117
133
  "microserviceChartClause",
118
134
  microserviceChartClause($)
119
135
  );
136
+ $.policyKeyFn = $.RULE("policyKeyFn", policyKeyFn($));
137
+ $.policyFieldFn = $.RULE("policyFieldFn", policyFieldFn($));
138
+ $.policyBlockBodyFn = $.RULE("policyBlockBodyFn", policyBlockBodyFn($));
139
+ $.policyBlockFn = $.RULE("policyBlockFn", policyBlockFn($));
140
+ $.policyDeclaration = $.RULE("policyDeclaration", policyDeclaration($));
120
141
  $.middlewareDeclaration = $.RULE(
121
142
  "middlewareDeclaration",
122
143
  middlewareDeclaration($)
@@ -5,16 +5,20 @@ import { InstructionsToken, ToolToken } from "../lexer/tool.js";
5
5
 
6
6
  export function toolDeclaration($) {
7
7
  return () => {
8
+ let hasAnnotations = false;
8
9
  let hasDescription = false;
9
10
  let hasGuid = false;
10
11
  let hasDocs = false;
11
12
  let hasHistory = false;
13
+ let hasIcons = false;
12
14
  let hasInput = false;
13
15
  let hasInstructions = false;
14
16
  let hasMiddleware = false;
17
+ let hasOutput = false;
15
18
  let hasResponse = false;
16
19
  let hasStack = false;
17
20
  let hasTags = false;
21
+ let hasTitle = false;
18
22
 
19
23
  $.sectionStack.push("toolDeclaration");
20
24
  // Allow leading comments and newlines before the tool declaration
@@ -31,6 +35,13 @@ export function toolDeclaration($) {
31
35
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
32
36
  $.OR2([
33
37
  { ALT: () => $.SUBRULE($.commentBlockFn) },
38
+ {
39
+ GATE: () => !hasAnnotations,
40
+ ALT: () => {
41
+ hasAnnotations = true;
42
+ $.SUBRULE($.mcpToolAnnotationsClause);
43
+ },
44
+ },
34
45
  {
35
46
  GATE: () => !hasDescription,
36
47
  ALT: () => {
@@ -59,6 +70,13 @@ export function toolDeclaration($) {
59
70
  $.SUBRULE($.historyClause);
60
71
  },
61
72
  },
73
+ {
74
+ GATE: () => !hasIcons,
75
+ ALT: () => {
76
+ hasIcons = true;
77
+ $.SUBRULE($.mcpIconsClause);
78
+ },
79
+ },
62
80
  {
63
81
  GATE: () => !hasInput,
64
82
  ALT: () => {
@@ -84,6 +102,13 @@ export function toolDeclaration($) {
84
102
  $.SUBRULE($.middlewareClause);
85
103
  },
86
104
  },
105
+ {
106
+ GATE: () => !hasOutput,
107
+ ALT: () => {
108
+ hasOutput = true;
109
+ $.SUBRULE($.outputClause);
110
+ },
111
+ },
87
112
  {
88
113
  GATE: () => !hasResponse,
89
114
  ALT: () => {
@@ -107,6 +132,13 @@ export function toolDeclaration($) {
107
132
  $.SUBRULE($.tagsAttribute);
108
133
  },
109
134
  },
135
+ {
136
+ GATE: () => !hasTitle,
137
+ ALT: () => {
138
+ hasTitle = true;
139
+ $.SUBRULE($.mcpTitleClause);
140
+ },
141
+ },
110
142
  ]);
111
143
  });
112
144
 
package/server.js CHANGED
@@ -17,6 +17,8 @@ import { onHover } from "./onHover/onHover.js";
17
17
  import { onSemanticCheck } from "./onSemanticCheck/onSemanticCheck.js";
18
18
  import { TOKEN_TYPES } from "./onSemanticCheck/tokens.js";
19
19
  import { xanoscriptParser } from "./parser/parser.js";
20
+ import { setPolicyCatalogue } from "./parser/policy/catalogue.js";
21
+ import policyCatalogueSnapshot from "./parser/policy/catalogueSnapshot.js";
20
22
  import { getSchemeFromContent } from "./utils.js";
21
23
  import { workspaceIndex } from "./workspace/workspaceIndex.js";
22
24
 
@@ -34,6 +36,10 @@ const FEATURES = {
34
36
  variableValidation: true, // variable usage validation
35
37
  };
36
38
 
39
+ // Policy rules are checked against the check catalogue this package was built with (see
40
+ // scripts/gen-policy-catalogue.mjs); what it finds is a warning, since the instance may be newer.
41
+ setPolicyCatalogue(policyCatalogueSnapshot, { source: "snapshot" });
42
+
37
43
  // Create a connection to the VS Code client
38
44
  const connection = createConnection(ProposedFeatures.all);
39
45
 
package/utils.js CHANGED
@@ -32,11 +32,14 @@ const schemeByFirstWord = {
32
32
  message: "message",
33
33
  microservice: "microservice",
34
34
  middleware: "middleware",
35
+ policy: "policy",
36
+ prompt: "prompt",
35
37
  query: "api",
36
38
  realtime_server: "realtime_server",
37
39
  realtime_server_trigger: "realtime_server_trigger",
38
40
  realtime_trigger: "realtime_trigger",
39
41
  realtime_channel: "realtime_channel",
42
+ resource: "resource",
40
43
  run: "run",
41
44
  table: "db",
42
45
  table_trigger: "table_trigger",
@@ -70,8 +73,9 @@ const typeByScheme = {
70
73
  // All other schemes map 1:1 (addon, agent, task, etc.)
71
74
  };
72
75
 
73
- // Extends firstWordRegex to also capture the name (quoted or bare identifier)
74
- const objectInfoRegex = /^(?:\s|\/\/[^\n]*\n)*(\w+)\s+(?:"([^"]+)"|(\w+))/;
76
+ // Extends firstWordRegex to also capture the name (quoted or bare identifier; a bare policy key
77
+ // may also hold "." and "-", e.g. `policy AUTH-001 {`)
78
+ const objectInfoRegex = /^(?:\s|\/\/[^\n]*\n)*(\w+)\s+(?:"([^"]+)"|(\w[\w.-]*))/;
75
79
 
76
80
  /**
77
81
  * Extract the object type and name from XanoScript file content.