@probelabs/visor 0.1.132-ee → 0.1.137

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 (140) hide show
  1. package/dist/config/config-reloader.d.ts +1 -0
  2. package/dist/config/config-reloader.d.ts.map +1 -1
  3. package/dist/config/config-watcher.d.ts +1 -0
  4. package/dist/config/config-watcher.d.ts.map +1 -1
  5. package/dist/config.d.ts +4 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/docs/ai-custom-tools-usage.md +37 -0
  8. package/dist/docs/ai-custom-tools.md +43 -1
  9. package/dist/docs/custom-tools.md +70 -1
  10. package/dist/docs/script.md +542 -27
  11. package/dist/docs/testing/cookbook.md +47 -0
  12. package/dist/examples/README.md +4 -0
  13. package/dist/examples/api-tools-ai-example.yaml +63 -0
  14. package/dist/examples/api-tools-inline-overlay-example.yaml +126 -0
  15. package/dist/examples/api-tools-library.yaml +18 -0
  16. package/dist/examples/api-tools-mcp-example.yaml +55 -0
  17. package/dist/examples/openapi/profiles-overlay-rename.yaml +3 -0
  18. package/dist/examples/openapi/users-api.json +91 -0
  19. package/dist/examples/openapi/users-overlay-rename.yaml +3 -0
  20. package/dist/generated/config-schema.d.ts +223 -74
  21. package/dist/generated/config-schema.d.ts.map +1 -1
  22. package/dist/generated/config-schema.json +251 -79
  23. package/dist/index.js +48417 -29987
  24. package/dist/output/traces/run-2026-02-23T08-59-32-321Z.ndjson +138 -0
  25. package/dist/output/traces/run-2026-02-23T09-00-20-148Z.ndjson +1442 -0
  26. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  27. package/dist/providers/api-tool-executor.d.ts +43 -0
  28. package/dist/providers/api-tool-executor.d.ts.map +1 -0
  29. package/dist/providers/command-check-provider.d.ts.map +1 -1
  30. package/dist/providers/custom-tool-executor.d.ts +21 -0
  31. package/dist/providers/custom-tool-executor.d.ts.map +1 -1
  32. package/dist/providers/mcp-check-provider.d.ts.map +1 -1
  33. package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -1
  34. package/dist/providers/script-check-provider.d.ts +18 -2
  35. package/dist/providers/script-check-provider.d.ts.map +1 -1
  36. package/dist/sdk/{check-provider-registry-7TCA3NSG.mjs → check-provider-registry-BCGP62RY.mjs} +9 -8
  37. package/dist/sdk/{check-provider-registry-KUDVEKAC.mjs → check-provider-registry-SA2WHPLO.mjs} +10 -9
  38. package/dist/sdk/check-provider-registry-SCL4KP55.mjs +29 -0
  39. package/dist/sdk/chunk-ALB3N4ZQ.mjs +443 -0
  40. package/dist/sdk/chunk-ALB3N4ZQ.mjs.map +1 -0
  41. package/dist/sdk/{chunk-27RV5RR2.mjs → chunk-BRD36I43.mjs} +3 -3
  42. package/dist/sdk/{chunk-2RNTEWOA.mjs → chunk-DFKP7LY6.mjs} +1901 -1767
  43. package/dist/sdk/chunk-DFKP7LY6.mjs.map +1 -0
  44. package/dist/sdk/{chunk-BGBXLPLL.mjs → chunk-E2N3U5HU.mjs} +5 -5
  45. package/dist/sdk/{chunk-XGI47XIH.mjs → chunk-F4K5WFSM.mjs} +1896 -1762
  46. package/dist/sdk/chunk-F4K5WFSM.mjs.map +1 -0
  47. package/dist/sdk/chunk-J6F5K5EG.mjs +40235 -0
  48. package/dist/sdk/chunk-J6F5K5EG.mjs.map +1 -0
  49. package/dist/sdk/{chunk-U3BLLEW3.mjs → chunk-KPRFDKQX.mjs} +329 -80
  50. package/dist/sdk/chunk-KPRFDKQX.mjs.map +1 -0
  51. package/dist/sdk/{chunk-VF6XIUE4.mjs → chunk-LW3INISN.mjs} +32 -1
  52. package/dist/sdk/{chunk-VF6XIUE4.mjs.map → chunk-LW3INISN.mjs.map} +1 -1
  53. package/dist/sdk/{chunk-VG7FWDC2.mjs → chunk-QUEWQWDX.mjs} +11 -4
  54. package/dist/sdk/{chunk-VG7FWDC2.mjs.map → chunk-QUEWQWDX.mjs.map} +1 -1
  55. package/dist/sdk/chunk-UMFEBYCN.mjs +1502 -0
  56. package/dist/sdk/chunk-UMFEBYCN.mjs.map +1 -0
  57. package/dist/sdk/chunk-XKCER23W.mjs +1490 -0
  58. package/dist/sdk/chunk-XKCER23W.mjs.map +1 -0
  59. package/dist/sdk/chunk-YTAGJZHN.mjs +739 -0
  60. package/dist/sdk/chunk-YTAGJZHN.mjs.map +1 -0
  61. package/dist/sdk/{chunk-XJQKTK6V.mjs → chunk-ZUEQNCKB.mjs} +2 -2
  62. package/dist/sdk/{config-FMIIATKX.mjs → config-3UIU4TMP.mjs} +3 -3
  63. package/dist/sdk/{failure-condition-evaluator-PNONVBXD.mjs → failure-condition-evaluator-3B3G5NYW.mjs} +4 -4
  64. package/dist/sdk/failure-condition-evaluator-B5JJFYKU.mjs +17 -0
  65. package/dist/sdk/{github-frontend-WR4S3NG5.mjs → github-frontend-VAWVSCNX.mjs} +4 -4
  66. package/dist/sdk/github-frontend-ZOVXPPHQ.mjs +1356 -0
  67. package/dist/sdk/github-frontend-ZOVXPPHQ.mjs.map +1 -0
  68. package/dist/sdk/{host-TROSAWTE.mjs → host-LOQWBHWT.mjs} +2 -2
  69. package/dist/sdk/{host-U7V54J2H.mjs → host-TEQ7HKKH.mjs} +2 -2
  70. package/dist/sdk/{liquid-extensions-YDIIH33Q.mjs → liquid-extensions-PLBOMRLI.mjs} +3 -3
  71. package/dist/sdk/{routing-F4FOWVKF.mjs → routing-HR6N43RQ.mjs} +6 -6
  72. package/dist/sdk/routing-SEQYM4N6.mjs +25 -0
  73. package/dist/sdk/schedule-tool-2COUUTF7.mjs +18 -0
  74. package/dist/sdk/{schedule-tool-handler-ULNF7TZW.mjs → schedule-tool-handler-5BDMLHS5.mjs} +10 -9
  75. package/dist/sdk/{schedule-tool-handler-VFES42DD.mjs → schedule-tool-handler-OXGTPLST.mjs} +9 -8
  76. package/dist/sdk/schedule-tool-handler-OXGTPLST.mjs.map +1 -0
  77. package/dist/sdk/schedule-tool-handler-Y2UABBXN.mjs +39 -0
  78. package/dist/sdk/schedule-tool-handler-Y2UABBXN.mjs.map +1 -0
  79. package/dist/sdk/sdk.d.mts +55 -2
  80. package/dist/sdk/sdk.d.ts +55 -2
  81. package/dist/sdk/sdk.js +2532 -1905
  82. package/dist/sdk/sdk.js.map +1 -1
  83. package/dist/sdk/sdk.mjs +9 -8
  84. package/dist/sdk/sdk.mjs.map +1 -1
  85. package/dist/sdk/{trace-helpers-RDPXIN4S.mjs → trace-helpers-FAAGLXBI.mjs} +2 -2
  86. package/dist/sdk/trace-helpers-FAAGLXBI.mjs.map +1 -0
  87. package/dist/sdk/trace-helpers-IGMH7ZPP.mjs +25 -0
  88. package/dist/sdk/trace-helpers-IGMH7ZPP.mjs.map +1 -0
  89. package/dist/sdk/{workflow-check-provider-4NFWH6YO.mjs → workflow-check-provider-7SR7ZWSV.mjs} +9 -8
  90. package/dist/sdk/workflow-check-provider-7SR7ZWSV.mjs.map +1 -0
  91. package/dist/sdk/{workflow-check-provider-5XS62BCJ.mjs → workflow-check-provider-L2ZUOMJR.mjs} +10 -9
  92. package/dist/sdk/workflow-check-provider-L2ZUOMJR.mjs.map +1 -0
  93. package/dist/sdk/workflow-check-provider-WLA7LO56.mjs +29 -0
  94. package/dist/sdk/workflow-check-provider-WLA7LO56.mjs.map +1 -0
  95. package/dist/state-machine/dispatch/execution-invoker.d.ts.map +1 -1
  96. package/dist/state-machine-execution-engine.d.ts.map +1 -1
  97. package/dist/test-runner/core/test-execution-wrapper.d.ts.map +1 -1
  98. package/dist/test-runner/index.d.ts.map +1 -1
  99. package/dist/test-runner/validator.d.ts.map +1 -1
  100. package/dist/traces/run-2026-02-23T08-59-32-321Z.ndjson +138 -0
  101. package/dist/traces/run-2026-02-23T09-00-20-148Z.ndjson +1442 -0
  102. package/dist/types/config.d.ts +55 -2
  103. package/dist/types/config.d.ts.map +1 -1
  104. package/dist/utils/config-loader.d.ts +5 -0
  105. package/dist/utils/config-loader.d.ts.map +1 -1
  106. package/dist/utils/sandbox.d.ts +8 -0
  107. package/dist/utils/sandbox.d.ts.map +1 -1
  108. package/dist/utils/script-tool-environment.d.ts +90 -0
  109. package/dist/utils/script-tool-environment.d.ts.map +1 -0
  110. package/dist/utils/tool-resolver.d.ts +18 -0
  111. package/dist/utils/tool-resolver.d.ts.map +1 -0
  112. package/package.json +11 -4
  113. package/dist/sdk/chunk-2RNTEWOA.mjs.map +0 -1
  114. package/dist/sdk/chunk-U3BLLEW3.mjs.map +0 -1
  115. package/dist/sdk/chunk-XGI47XIH.mjs.map +0 -1
  116. package/dist/sdk/knex-store-HPXJILBL.mjs +0 -411
  117. package/dist/sdk/knex-store-HPXJILBL.mjs.map +0 -1
  118. package/dist/sdk/loader-ZC5G3JGJ.mjs +0 -89
  119. package/dist/sdk/loader-ZC5G3JGJ.mjs.map +0 -1
  120. package/dist/sdk/opa-policy-engine-S2S2ULEI.mjs +0 -655
  121. package/dist/sdk/opa-policy-engine-S2S2ULEI.mjs.map +0 -1
  122. package/dist/sdk/validator-XTZJZZJH.mjs +0 -134
  123. package/dist/sdk/validator-XTZJZZJH.mjs.map +0 -1
  124. /package/dist/sdk/{check-provider-registry-7TCA3NSG.mjs.map → check-provider-registry-BCGP62RY.mjs.map} +0 -0
  125. /package/dist/sdk/{check-provider-registry-KUDVEKAC.mjs.map → check-provider-registry-SA2WHPLO.mjs.map} +0 -0
  126. /package/dist/sdk/{config-FMIIATKX.mjs.map → check-provider-registry-SCL4KP55.mjs.map} +0 -0
  127. /package/dist/sdk/{chunk-27RV5RR2.mjs.map → chunk-BRD36I43.mjs.map} +0 -0
  128. /package/dist/sdk/{chunk-BGBXLPLL.mjs.map → chunk-E2N3U5HU.mjs.map} +0 -0
  129. /package/dist/sdk/{chunk-XJQKTK6V.mjs.map → chunk-ZUEQNCKB.mjs.map} +0 -0
  130. /package/dist/sdk/{failure-condition-evaluator-PNONVBXD.mjs.map → config-3UIU4TMP.mjs.map} +0 -0
  131. /package/dist/sdk/{liquid-extensions-YDIIH33Q.mjs.map → failure-condition-evaluator-3B3G5NYW.mjs.map} +0 -0
  132. /package/dist/sdk/{routing-F4FOWVKF.mjs.map → failure-condition-evaluator-B5JJFYKU.mjs.map} +0 -0
  133. /package/dist/sdk/{github-frontend-WR4S3NG5.mjs.map → github-frontend-VAWVSCNX.mjs.map} +0 -0
  134. /package/dist/sdk/{host-TROSAWTE.mjs.map → host-LOQWBHWT.mjs.map} +0 -0
  135. /package/dist/sdk/{host-U7V54J2H.mjs.map → host-TEQ7HKKH.mjs.map} +0 -0
  136. /package/dist/sdk/{schedule-tool-handler-ULNF7TZW.mjs.map → liquid-extensions-PLBOMRLI.mjs.map} +0 -0
  137. /package/dist/sdk/{schedule-tool-handler-VFES42DD.mjs.map → routing-HR6N43RQ.mjs.map} +0 -0
  138. /package/dist/sdk/{trace-helpers-RDPXIN4S.mjs.map → routing-SEQYM4N6.mjs.map} +0 -0
  139. /package/dist/sdk/{workflow-check-provider-4NFWH6YO.mjs.map → schedule-tool-2COUUTF7.mjs.map} +0 -0
  140. /package/dist/sdk/{workflow-check-provider-5XS62BCJ.mjs.map → schedule-tool-handler-5BDMLHS5.mjs.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  ## Script Step (`type: script`)
2
2
 
3
- The `script` provider executes JavaScript in a secure sandbox with access to PR context, dependency outputs, workflow inputs, environment variables, and the Visor memory store.
3
+ The `script` provider executes JavaScript in a secure sandbox with access to PR context, dependency outputs, workflow inputs, environment variables, and the Visor memory store. Scripts can also call external tools, MCP servers, and built-in functions like `schedule()`, `fetch()`, `github()`, and `bash()`.
4
4
 
5
5
  ## Configuration
6
6
 
@@ -8,6 +8,12 @@ The `script` provider executes JavaScript in a secure sandbox with access to PR
8
8
  |----------|----------|-------------|
9
9
  | `type` | Yes | Must be `script` |
10
10
  | `content` | Yes | JavaScript code to execute (max 1MB) |
11
+ | `tools` | No | List of tool names or workflow references to expose as callable functions |
12
+ | `tools_js` | No | JavaScript expression to dynamically compute tools at runtime |
13
+ | `mcp_servers` | No | MCP servers whose tools are exposed as callable functions |
14
+ | `enable_fetch` | No | Enable the `fetch()` built-in for HTTP requests (default: `false`) |
15
+ | `enable_bash` | No | Enable the `bash()` built-in for shell commands (default: `false`) |
16
+ | `timeout` | No | Execution timeout in milliseconds (default: 60000) |
11
17
  | `depends_on` | No | Array of step IDs this step depends on |
12
18
  | `group` | No | Group name for organizing steps |
13
19
  | `on` | No | Event triggers for this step |
@@ -61,12 +67,399 @@ The secure sandbox exposes these objects and functions:
61
67
 
62
68
  The value you `return` from the script becomes the step's `output`, accessible to dependent steps via `outputs['step-name']`.
63
69
 
70
+ ---
71
+
72
+ ## Built-in Functions
73
+
74
+ Script steps have access to built-in async functions. You write normal synchronous-looking code — `await` is automatically injected by an AST transformer at compile time.
75
+
76
+ ### `schedule(args)` — Always Available
77
+
78
+ Create, list, cancel, pause, and resume scheduled workflows or reminders.
79
+
80
+ ```javascript
81
+ // Create a recurring schedule
82
+ const result = schedule({
83
+ action: 'create',
84
+ workflow: 'daily-review',
85
+ cron: '0 9 * * 1-5', // weekdays at 9am
86
+ is_recurring: true
87
+ });
88
+ log(result.success, result.message);
89
+
90
+ // List active schedules
91
+ const list = schedule({ action: 'list' });
92
+ log(list.schedules);
93
+
94
+ // Cancel a schedule
95
+ schedule({ action: 'cancel', schedule_id: 'abc123' });
96
+
97
+ // Pause / resume
98
+ schedule({ action: 'pause', schedule_id: 'abc123' });
99
+ schedule({ action: 'resume', schedule_id: 'abc123' });
100
+ ```
101
+
102
+ **Arguments for `action: 'create'`:**
103
+
104
+ | Field | Type | Description |
105
+ |-------|------|-------------|
106
+ | `action` | `string` | Required. One of: `create`, `list`, `cancel`, `pause`, `resume` |
107
+ | `workflow` | `string` | Workflow to execute on schedule |
108
+ | `workflow_inputs` | `object` | Inputs to pass to the workflow |
109
+ | `reminder_text` | `string` | Text reminder (if not running a workflow) |
110
+ | `cron` | `string` | Cron expression for recurring schedules (e.g., `"0 9 * * 1-5"`) |
111
+ | `run_at` | `string` | ISO 8601 timestamp for one-time schedules |
112
+ | `is_recurring` | `boolean` | Whether this is a recurring schedule |
113
+ | `schedule_id` | `string` | Schedule ID (for `cancel`, `pause`, `resume`) |
114
+
115
+ **Returns:** `{ success: boolean, message: string, schedule?: object, schedules?: object[], error?: string }`
116
+
117
+ ### `fetch(args)` — Requires `enable_fetch: true`
118
+
119
+ Make HTTP requests from scripts. Responses are automatically parsed as JSON when the Content-Type header indicates JSON.
120
+
121
+ ```yaml
122
+ checks:
123
+ call-api:
124
+ type: script
125
+ enable_fetch: true
126
+ content: |
127
+ const data = fetch({
128
+ url: 'https://api.example.com/data',
129
+ headers: { Authorization: 'Bearer ' + env.API_TOKEN }
130
+ });
131
+ return data;
132
+ ```
133
+
134
+ **Arguments:**
135
+
136
+ | Field | Type | Default | Description |
137
+ |-------|------|---------|-------------|
138
+ | `url` | `string` | — | Required. The URL to fetch |
139
+ | `method` | `string` | `"GET"` | HTTP method |
140
+ | `headers` | `object` | `{}` | Request headers |
141
+ | `body` | `string` | — | Request body (ignored for GET) |
142
+ | `timeout` | `number` | `30000` | Timeout in milliseconds |
143
+
144
+ **Returns:** Parsed JSON object, or string for non-JSON responses. Returns `"ERROR: ..."` on failure.
145
+
146
+ ```javascript
147
+ // POST with JSON body
148
+ const result = fetch({
149
+ url: 'https://api.example.com/webhook',
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({ event: 'deploy', version: '1.2.3' })
153
+ });
154
+
155
+ // GET with query params
156
+ const users = fetch({
157
+ url: 'https://api.example.com/users?role=admin',
158
+ headers: { Authorization: 'Bearer ' + env.API_TOKEN },
159
+ timeout: 10000
160
+ });
161
+ ```
162
+
163
+ ### `github(args)` — Available in GitHub Context
164
+
165
+ Perform GitHub operations (labels, comments) directly from scripts. This function is only available when running in a GitHub context (GitHub Actions, PR events) where an authenticated Octokit instance exists.
166
+
167
+ ```yaml
168
+ checks:
169
+ label-pr:
170
+ type: script
171
+ content: |
172
+ // Add labels based on file changes
173
+ const hasTests = pr.files.some(f => f.filename.includes('test'));
174
+ const labels = [];
175
+ if (hasTests) labels.push('has-tests');
176
+ if (pr.totalAdditions > 500) labels.push('large-pr');
177
+
178
+ if (labels.length > 0) {
179
+ github({ op: 'labels.add', values: labels });
180
+ }
181
+
182
+ return { labels };
183
+ ```
184
+
185
+ **Arguments:**
186
+
187
+ | Field | Type | Description |
188
+ |-------|------|-------------|
189
+ | `op` | `string` | Required. Operation: `labels.add`, `labels.remove`, or `comment.create` |
190
+ | `values` | `string[]` | Array of values (label names or comment text) |
191
+ | `value` | `string` | Single value (alternative to `values`) |
192
+
193
+ **Supported operations:**
194
+
195
+ | Operation | Description | Values |
196
+ |-----------|-------------|--------|
197
+ | `labels.add` | Add labels to the PR | Array of label names |
198
+ | `labels.remove` | Remove labels from the PR | Array of label names |
199
+ | `comment.create` | Post a comment on the PR | Single comment body string |
200
+
201
+ **Returns:** `{ success: true, op: string }` or `"ERROR: ..."` on failure.
202
+
203
+ ```javascript
204
+ // Add labels
205
+ github({ op: 'labels.add', values: ['reviewed', 'approved'] });
206
+
207
+ // Remove a label
208
+ github({ op: 'labels.remove', value: 'needs-review' });
209
+
210
+ // Post a comment
211
+ github({
212
+ op: 'comment.create',
213
+ value: '## Automated Review\nAll checks passed.'
214
+ });
215
+ ```
216
+
217
+ ### `bash(args)` — Requires `enable_bash: true`
218
+
219
+ Execute shell commands from scripts. This is gated behind the `enable_bash` flag for security.
220
+
221
+ ```yaml
222
+ checks:
223
+ run-analysis:
224
+ type: script
225
+ enable_bash: true
226
+ content: |
227
+ const result = bash({ command: 'wc -l src/**/*.ts' });
228
+ log('stdout:', result.stdout);
229
+ log('exit code:', result.exitCode);
230
+
231
+ if (result.exitCode !== 0) {
232
+ return { error: result.stderr };
233
+ }
234
+ return { lineCount: result.stdout.trim() };
235
+ ```
236
+
237
+ **Arguments:**
238
+
239
+ | Field | Type | Default | Description |
240
+ |-------|------|---------|-------------|
241
+ | `command` | `string` | — | Required. Shell command to execute |
242
+ | `cwd` | `string` | — | Working directory |
243
+ | `env` | `object` | — | Additional environment variables |
244
+ | `timeout` | `number` | `30000` | Timeout in milliseconds |
245
+
246
+ **Returns:** `{ stdout: string, stderr: string, exitCode: number }` or `"ERROR: ..."` on failure.
247
+
248
+ ```javascript
249
+ // Run a command with custom working directory
250
+ const out = bash({
251
+ command: 'npm test -- --coverage',
252
+ cwd: '/workspace/project',
253
+ timeout: 120000
254
+ });
255
+
256
+ // Chain commands
257
+ const build = bash({ command: 'npm run build && npm run lint' });
258
+ if (build.exitCode !== 0) {
259
+ return { success: false, error: build.stderr };
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## External Tools
266
+
267
+ Script steps can call external tools defined in the global `tools:` section, workflow tools, and MCP server tools. Tools are exposed as regular functions — just call them by name.
268
+
269
+ ### Configuring Tools
270
+
271
+ Use `tools` to reference global tools and workflows, and `mcp_servers` for MCP server tools:
272
+
273
+ ```yaml
274
+ tools:
275
+ fetch-jira:
276
+ name: fetch-jira
277
+ description: Fetch a Jira ticket by key
278
+ exec: 'curl -s -H "Authorization: Bearer ${JIRA_TOKEN}" https://jira.example.com/rest/api/2/issue/{{ args.key }}'
279
+ parseJson: true
280
+ inputSchema:
281
+ type: object
282
+ properties:
283
+ key: { type: string, description: Jira issue key }
284
+ required: [key]
285
+
286
+ run-linter:
287
+ name: run-linter
288
+ exec: 'eslint {{ args.file }} --format json'
289
+ parseJson: true
290
+ inputSchema:
291
+ type: object
292
+ properties:
293
+ file: { type: string }
294
+
295
+ checks:
296
+ analyze:
297
+ type: script
298
+ tools:
299
+ - fetch-jira
300
+ - run-linter
301
+ content: |
302
+ // Tools are available as functions — just call them by name
303
+ const ticket = fetch_jira({ key: 'PROJ-123' });
304
+ log('Ticket:', ticket.fields.summary);
305
+
306
+ const lint = run_linter({ file: 'src/index.ts' });
307
+ return { ticket: ticket.fields.summary, lintErrors: lint.length };
308
+ ```
309
+
310
+ ### Tool Naming Convention
311
+
312
+ Tool names are converted to valid JavaScript identifiers:
313
+ - Hyphens in tool names become underscores: `fetch-jira` → `fetch_jira()`
314
+ - MCP tools are prefixed with the server name: `github` server's `get_pull_request` tool → `github_get_pull_request()`
315
+
316
+ ### Using MCP Server Tools
317
+
318
+ Connect to external MCP servers and call their tools directly:
319
+
320
+ ```yaml
321
+ checks:
322
+ mcp-analysis:
323
+ type: script
324
+ mcp_servers:
325
+ github:
326
+ command: github-mcp-server
327
+ args: [--token, "${GITHUB_TOKEN}"]
328
+ jira:
329
+ command: npx
330
+ args: [-y, "@aashari/mcp-server-atlassian-jira"]
331
+ env:
332
+ ATLASSIAN_SITE_NAME: mysite
333
+ content: |
334
+ // MCP tools are namespaced: {serverName}_{toolName}
335
+ const pr = github_get_pull_request({
336
+ owner: 'myorg',
337
+ repo: 'myrepo',
338
+ pull_number: pr.number
339
+ });
340
+
341
+ const issues = jira_search_issues({
342
+ jql: 'project = PROJ AND status = Open'
343
+ });
344
+
345
+ return { pr: pr.title, openIssues: issues.total };
346
+ ```
347
+
348
+ MCP servers support three transport types:
349
+
350
+ | Property | Description |
351
+ |----------|-------------|
352
+ | `command` + `args` | Stdio transport — spawn an MCP server as a subprocess |
353
+ | `url` | SSE or HTTP transport — connect to a remote MCP server |
354
+ | `transport` | Explicit transport type: `stdio`, `sse`, or `http` |
355
+ | `env` | Environment variables passed to the MCP server process |
356
+ | `allowedMethods` | Whitelist specific tools (supports wildcards: `search_*`) |
357
+ | `blockedMethods` | Block specific tools (supports wildcards: `*_delete`) |
358
+
359
+ ### Using Workflow Tools
360
+
361
+ Reference other workflows as tools:
362
+
363
+ ```yaml
364
+ checks:
365
+ orchestrate:
366
+ type: script
367
+ tools:
368
+ - workflow: security-scan
369
+ args: { depth: full }
370
+ - workflow: lint-check
371
+ content: |
372
+ const security = security_scan({ target: 'src/' });
373
+ const lint = lint_check({ files: pr.files.map(f => f.filename) });
374
+
375
+ return {
376
+ secure: security.passed,
377
+ clean: lint.errors === 0
378
+ };
379
+ ```
380
+
381
+ ### Dynamic Tools with `tools_js`
382
+
383
+ Compute tools dynamically based on dependency outputs or context:
384
+
385
+ ```yaml
386
+ checks:
387
+ dynamic-step:
388
+ type: script
389
+ depends_on: [route-intent]
390
+ tools_js: |
391
+ const tools = [];
392
+ const tags = outputs['route-intent']?.tags || [];
393
+ if (tags.includes('jira')) tools.push('fetch-jira');
394
+ if (tags.includes('security')) tools.push('run-security-scan');
395
+ return tools;
396
+ content: |
397
+ // Only the dynamically selected tools are available
398
+ const result = fetch_jira({ key: 'PROJ-456' });
399
+ return result;
400
+ ```
401
+
402
+ ### Tool Discovery
403
+
404
+ Use `listTools()` to see all available tools at runtime:
405
+
406
+ ```yaml
407
+ checks:
408
+ discover:
409
+ type: script
410
+ tools: [fetch-jira, run-linter]
411
+ content: |
412
+ const tools = listTools();
413
+ log('Available tools:', tools.map(t => t.name));
414
+ // [{ name: 'fetch_jira', description: '...' }, { name: 'run_linter', description: '...' }]
415
+ return { toolCount: tools.length };
416
+ ```
417
+
418
+ You can also use `callTool(name, args)` as an alternative to calling tools by name directly:
419
+
420
+ ```javascript
421
+ const result = callTool('fetch_jira', { key: 'PROJ-123' });
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Error Handling
427
+
428
+ All built-in functions and tools return `"ERROR: ..."` strings on failure instead of throwing exceptions. This makes error handling straightforward:
429
+
430
+ ```javascript
431
+ const result = fetch_jira({ key: 'INVALID' });
432
+
433
+ if (typeof result === 'string' && result.startsWith('ERROR:')) {
434
+ log('Tool failed:', result);
435
+ return { success: false, error: result };
436
+ }
437
+
438
+ return { success: true, data: result };
439
+ ```
440
+
441
+ This pattern is consistent across all functions: `schedule()`, `fetch()`, `github()`, `bash()`, custom tools, and MCP tools.
442
+
443
+ ---
444
+
445
+ ## Security Considerations
446
+
447
+ - **Sandbox Isolation**: Scripts run in a secure sandbox (`@nyariv/sandboxjs`) with no access to `process`, `require`, `fs`, or other Node.js globals
448
+ - **`enable_bash`**: Shell execution is disabled by default. Enable only when needed and be aware that bash commands run with the Visor process permissions
449
+ - **`enable_fetch`**: HTTP access is disabled by default. Enable only when the script needs to call external APIs
450
+ - **`github()`**: Only available when an authenticated Octokit instance exists in the execution context (GitHub Actions). Cannot be enabled manually
451
+ - **MCP Servers**: Use `allowedMethods` and `blockedMethods` to restrict which tools a server exposes
452
+ - **Tool Inputs**: Global tools with `inputSchema` validate arguments before execution
453
+ - **Loop Protection**: `while`, `for`, `for...of`, and `for...in` loops are capped at 10,000 iterations
454
+
455
+ ---
456
+
64
457
  ## Examples
65
458
 
66
459
  ### Basic Example
67
460
 
68
461
  ```yaml
69
- steps:
462
+ checks:
70
463
  extract-facts:
71
464
  type: command
72
465
  exec: node ./scripts/extract-facts.js
@@ -85,7 +478,7 @@ steps:
85
478
  ### Using PR Context
86
479
 
87
480
  ```yaml
88
- steps:
481
+ checks:
89
482
  analyze-pr:
90
483
  type: script
91
484
  content: |
@@ -100,10 +493,126 @@ steps:
100
493
  };
101
494
  ```
102
495
 
496
+ ### Fetching External Data and Labeling
497
+
498
+ ```yaml
499
+ checks:
500
+ enrich-pr:
501
+ type: script
502
+ enable_fetch: true
503
+ content: |
504
+ // Fetch deployment status from an external API
505
+ const status = fetch({
506
+ url: 'https://deploy.example.com/api/status/' + pr.head,
507
+ headers: { Authorization: 'Bearer ' + env.DEPLOY_TOKEN }
508
+ });
509
+
510
+ if (typeof status === 'string' && status.startsWith('ERROR:')) {
511
+ return { deployed: false, error: status };
512
+ }
513
+
514
+ // Label the PR based on deployment status
515
+ if (status.state === 'deployed') {
516
+ github({ op: 'labels.add', values: ['deployed'] });
517
+ }
518
+
519
+ return { deployed: status.state === 'deployed', environment: status.env };
520
+ ```
521
+
522
+ ### Running Tests and Scheduling Follow-up
523
+
524
+ ```yaml
525
+ checks:
526
+ test-and-schedule:
527
+ type: script
528
+ enable_bash: true
529
+ content: |
530
+ // Run tests
531
+ const result = bash({
532
+ command: 'npm test -- --coverage --json',
533
+ timeout: 120000
534
+ });
535
+
536
+ if (result.exitCode !== 0) {
537
+ // Schedule a retry in 30 minutes
538
+ schedule({
539
+ action: 'create',
540
+ workflow: 'test-and-schedule',
541
+ run_at: new Date(Date.now() + 30 * 60 * 1000).toISOString()
542
+ });
543
+ return { passed: false, stderr: result.stderr };
544
+ }
545
+
546
+ return { passed: true, output: result.stdout };
547
+ ```
548
+
549
+ ### Multi-Tool Orchestration
550
+
551
+ ```yaml
552
+ tools:
553
+ fetch-jira:
554
+ name: fetch-jira
555
+ description: Fetch Jira ticket details
556
+ exec: 'curl -s -H "Authorization: Bearer ${JIRA_TOKEN}" https://jira.example.com/rest/api/2/issue/{{ args.key }}'
557
+ parseJson: true
558
+ inputSchema:
559
+ type: object
560
+ properties:
561
+ key: { type: string }
562
+ required: [key]
563
+
564
+ checks:
565
+ full-analysis:
566
+ type: script
567
+ enable_fetch: true
568
+ tools:
569
+ - fetch-jira
570
+ mcp_servers:
571
+ github:
572
+ command: github-mcp-server
573
+ args: [--token, "${GITHUB_TOKEN}"]
574
+ content: |
575
+ // Extract Jira key from PR title (e.g., "PROJ-123: Fix bug")
576
+ const match = pr.title.match(/([A-Z]+-\d+)/);
577
+ if (!match) return { jira: null, message: 'No Jira key in PR title' };
578
+
579
+ // Fetch Jira ticket using a custom tool
580
+ const ticket = fetch_jira({ key: match[1] });
581
+ if (typeof ticket === 'string' && ticket.startsWith('ERROR:')) {
582
+ return { error: ticket };
583
+ }
584
+
585
+ // Get reviewer suggestions from an API
586
+ const reviewers = fetch({
587
+ url: 'https://internal.example.com/api/reviewers',
588
+ method: 'POST',
589
+ headers: { 'Content-Type': 'application/json' },
590
+ body: JSON.stringify({
591
+ component: ticket.fields.components?.[0]?.name,
592
+ files: pr.files.map(f => f.filename)
593
+ })
594
+ });
595
+
596
+ // Label PR with Jira status
597
+ github({
598
+ op: 'labels.add',
599
+ values: ['jira:' + ticket.fields.status.name.toLowerCase()]
600
+ });
601
+
602
+ return {
603
+ jira: {
604
+ key: match[1],
605
+ summary: ticket.fields.summary,
606
+ status: ticket.fields.status.name
607
+ },
608
+ suggestedReviewers: reviewers?.users || []
609
+ };
610
+ ```
611
+
103
612
  ### Using Output History
104
613
 
105
614
  ```yaml
106
- steps:
615
+ checks:
107
616
  track-retries:
108
617
  type: script
109
618
  depends_on: [some-check]
@@ -129,7 +638,7 @@ inputs:
129
638
  - name: threshold
130
639
  default: 10
131
640
 
132
- steps:
641
+ checks:
133
642
  check-threshold:
134
643
  type: script
135
644
  content: |
@@ -144,38 +653,44 @@ steps:
144
653
  };
145
654
  ```
146
655
 
147
- ### Complex Data Processing
656
+ ---
148
657
 
149
- ```yaml
150
- steps:
151
- process-results:
152
- type: script
153
- depends_on: [run-tests]
154
- content: |
155
- const results = outputs['run-tests'];
658
+ ## How Async Works
156
659
 
157
- // Calculate statistics
158
- const stats = {
159
- total: results.tests?.length || 0,
160
- passed: results.tests?.filter(t => t.passed).length || 0,
161
- failed: results.tests?.filter(t => !t.passed).length || 0
162
- };
660
+ You don't need to write `async`/`await` yourself. The script engine uses an AST transformer that:
163
661
 
164
- stats.passRate = stats.total > 0
165
- ? ((stats.passed / stats.total) * 100).toFixed(2)
166
- : 0;
662
+ 1. Parses your code to find calls to async functions (`schedule`, `fetch`, `github`, `bash`, tool names, MCP tools)
663
+ 2. Automatically injects `await` before those calls
664
+ 3. Wraps the script in an `async` IIFE for execution
167
665
 
168
- // Store in memory for other steps
169
- memory.set('test_stats', stats);
666
+ So this code:
170
667
 
171
- // Debug logging
172
- log('Test stats:', stats);
668
+ ```javascript
669
+ const data = fetch({ url: 'https://api.example.com/data' });
670
+ const ticket = fetch_jira({ key: 'PROJ-123' });
671
+ return { data, ticket };
672
+ ```
673
+
674
+ Is transparently transformed to:
173
675
 
174
- return stats;
676
+ ```javascript
677
+ return (async () => {
678
+ const data = await fetch({ url: 'https://api.example.com/data' });
679
+ const ticket = await fetch_jira({ key: 'PROJ-123' });
680
+ return { data, ticket };
681
+ })()
175
682
  ```
176
683
 
684
+ Callbacks inside `.map()` are also handled — the transformer marks them as `async` when they contain tool calls.
685
+
686
+ ---
687
+
177
688
  ## Related Documentation
178
689
 
690
+ - [Custom Tools](./custom-tools.md) - Define reusable command-line tools in YAML
691
+ - [AI Custom Tools](./ai-custom-tools.md) - Using custom tools with AI providers
692
+ - [MCP Provider](./mcp-provider.md) - Direct MCP tool execution
693
+ - [GitHub Operations](./github-ops.md) - Native GitHub operations provider
179
694
  - [Memory Provider](./memory.md) - Persistent key-value storage
180
695
  - [Output History](./output-history.md) - Tracking outputs across iterations
181
696
  - [Dependencies](./dependencies.md) - Step dependency system
@@ -193,6 +193,53 @@ With two facts extracted where only one is invalid, the correction pass runs for
193
193
  message_contains: "github/op_failed"
194
194
  ```
195
195
 
196
+ ## 9) API tool (`type: api`) with YAML tests
197
+
198
+ You can verify OpenAPI-to-MCP conversion without real network calls by asserting generated-tool validation behavior:
199
+
200
+ ```yaml
201
+ tools:
202
+ users-api:
203
+ type: api
204
+ name: users-api
205
+ spec: ./fixtures/api-tool-openapi.json
206
+ headers:
207
+ Authorization: "Bearer ${API_TEST_BEARER_TOKEN}"
208
+ X-Tenant-Id: "${API_TEST_TENANT_ID}"
209
+ whitelist: [get*]
210
+
211
+ checks:
212
+ api-tool-missing-required-input:
213
+ type: mcp
214
+ transport: custom
215
+ method: getUser
216
+ methodArgs: {}
217
+ on: [manual]
218
+
219
+ tests:
220
+ cases:
221
+ - name: api-tool-generated-operation-validates-input
222
+ event: manual
223
+ fixture: gh.pr_open.minimal
224
+ expect:
225
+ calls:
226
+ - step: api-tool-missing-required-input
227
+ exactly: 1
228
+ outputs:
229
+ - step: api-tool-missing-required-input
230
+ path: issues[0].message
231
+ matches: "(?i)required property 'id'"
232
+ ```
233
+
234
+ This confirms generated operation tools are registered and invoked through `transport: custom`.
235
+ The same config supports env-backed custom headers (for example `Authorization: "Bearer ${API_TEST_BEARER_TOKEN}"`).
236
+
237
+ Also see end-to-end example suites:
238
+
239
+ - `examples/api-tools-mcp-example.yaml` (embedded tests)
240
+ - `examples/api-tools-ai-example.yaml` (embedded tests)
241
+ - `examples/api-tools-inline-overlay-example.yaml` (embedded tests)
242
+
196
243
  ## Related Documentation
197
244
 
198
245
  - [Getting Started](./getting-started.md) - Introduction to the test framework
@@ -81,6 +81,7 @@ Example configurations demonstrating various Visor features and use cases.
81
81
  ### AI Provider Examples
82
82
  - **`ai-custom-tools-example.yaml`** - AI with custom shell-based tools via ephemeral MCP servers
83
83
  - **`ai-custom-tools-simple.yaml`** - Simplified AI custom tools example
84
+ - **`api-tools-ai-example.yaml`** - AI check using reusable OpenAPI-backed `type: api` tool bundles (includes embedded tests)
84
85
  - **`ai-retry-fallback-config.yaml`** - AI with retry and fallback providers
85
86
  - **`ai-with-bash.yaml`** - AI check combined with bash command execution
86
87
  - **`ai-with-mcp.yaml`** - AI check with MCP tool integration
@@ -91,6 +92,9 @@ Example configurations demonstrating various Visor features and use cases.
91
92
  ### MCP & Tools Examples
92
93
  - **`mcp-provider-example.yaml`** - MCP provider with stdio/SSE/HTTP transports
93
94
  - **`custom-tools-example.yaml`** - Custom tool definitions
95
+ - **`api-tools-library.yaml`** - Reusable OpenAPI API tool bundle definitions (`type: api`)
96
+ - **`api-tools-mcp-example.yaml`** - MCP checks calling OpenAPI-generated operations from reusable API bundle (includes embedded tests)
97
+ - **`api-tools-inline-overlay-example.yaml`** - Inline/file OpenAPI overlay example with reusable API tools (includes embedded tests)
94
98
  - **`tools-library.yaml`** - Reusable tool library (git, docker, npm, testing, code quality tools)
95
99
  - **`reusable-tools.yaml`** - Patterns for reusable tool definitions
96
100
  - **`project-with-tools.yaml`** - Project configuration with integrated tools