@probelabs/visor 0.1.129 → 0.1.130

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 (206) hide show
  1. package/README.md +23 -0
  2. package/dist/cli-main.d.ts.map +1 -1
  3. package/dist/config.d.ts +4 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/docs/author-permissions.md +20 -0
  6. package/dist/docs/enterprise-policy.md +1325 -0
  7. package/dist/docs/index.md +10 -0
  8. package/dist/docs/scheduler-storage.md +433 -0
  9. package/dist/docs/scheduler.md +12 -2
  10. package/dist/enterprise/license/validator.d.ts +39 -0
  11. package/dist/enterprise/license/validator.d.ts.map +1 -0
  12. package/dist/enterprise/loader.d.ts +25 -0
  13. package/dist/enterprise/loader.d.ts.map +1 -0
  14. package/dist/enterprise/policy/opa-compiler.d.ts +37 -0
  15. package/dist/enterprise/policy/opa-compiler.d.ts.map +1 -0
  16. package/dist/enterprise/policy/opa-http-evaluator.d.ts +36 -0
  17. package/dist/enterprise/policy/opa-http-evaluator.d.ts.map +1 -0
  18. package/dist/enterprise/policy/opa-policy-engine.d.ts +48 -0
  19. package/dist/enterprise/policy/opa-policy-engine.d.ts.map +1 -0
  20. package/dist/enterprise/policy/opa-wasm-evaluator.d.ts +34 -0
  21. package/dist/enterprise/policy/opa-wasm-evaluator.d.ts.map +1 -0
  22. package/dist/enterprise/policy/policy-input-builder.d.ts +120 -0
  23. package/dist/enterprise/policy/policy-input-builder.d.ts.map +1 -0
  24. package/dist/enterprise/scheduler/knex-store.d.ts +41 -0
  25. package/dist/enterprise/scheduler/knex-store.d.ts.map +1 -0
  26. package/dist/examples/README.md +23 -0
  27. package/dist/examples/enterprise-policy/README.md +344 -0
  28. package/dist/examples/enterprise-policy/policies/capability_resolve.rego +29 -0
  29. package/dist/examples/enterprise-policy/policies/capability_resolve_test.rego +230 -0
  30. package/dist/examples/enterprise-policy/policies/check_execute.rego +71 -0
  31. package/dist/examples/enterprise-policy/policies/check_execute_test.rego +321 -0
  32. package/dist/examples/enterprise-policy/policies/deploy_production.rego +33 -0
  33. package/dist/examples/enterprise-policy/policies/deploy_production_test.rego +29 -0
  34. package/dist/examples/enterprise-policy/policies/slack_channel_gate.rego +17 -0
  35. package/dist/examples/enterprise-policy/policies/slack_tool_restrict.rego +16 -0
  36. package/dist/examples/enterprise-policy/policies/tool_invoke.rego +24 -0
  37. package/dist/examples/enterprise-policy/policies/tool_invoke_test.rego +227 -0
  38. package/dist/examples/enterprise-policy/visor.yaml +64 -0
  39. package/dist/failure-condition-evaluator.d.ts +18 -0
  40. package/dist/failure-condition-evaluator.d.ts.map +1 -1
  41. package/dist/frontends/slack-frontend.d.ts +1 -0
  42. package/dist/frontends/slack-frontend.d.ts.map +1 -1
  43. package/dist/generated/config-schema.d.ts +139 -0
  44. package/dist/generated/config-schema.d.ts.map +1 -1
  45. package/dist/index.js +12121 -7169
  46. package/dist/liquid-extensions.d.ts.map +1 -1
  47. package/dist/output/traces/{run-2026-02-08T18-16-04-160Z.ndjson → run-2026-02-11T16-20-59-999Z.ndjson} +84 -84
  48. package/dist/{traces/run-2026-02-08T18-16-51-253Z.ndjson → output/traces/run-2026-02-11T16-21-47-711Z.ndjson} +1032 -1032
  49. package/dist/policy/default-engine.d.ts +17 -0
  50. package/dist/policy/default-engine.d.ts.map +1 -0
  51. package/dist/policy/index.d.ts +4 -0
  52. package/dist/policy/index.d.ts.map +1 -0
  53. package/dist/policy/policy-check-command.d.ts +65 -0
  54. package/dist/policy/policy-check-command.d.ts.map +1 -0
  55. package/dist/policy/types.d.ts +81 -0
  56. package/dist/policy/types.d.ts.map +1 -0
  57. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  58. package/dist/providers/check-provider.interface.d.ts +2 -0
  59. package/dist/providers/check-provider.interface.d.ts.map +1 -1
  60. package/dist/providers/claude-code-check-provider.d.ts.map +1 -1
  61. package/dist/providers/mcp-check-provider.d.ts.map +1 -1
  62. package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -1
  63. package/dist/providers/workflow-check-provider.d.ts.map +1 -1
  64. package/dist/scheduler/index.d.ts +2 -0
  65. package/dist/scheduler/index.d.ts.map +1 -1
  66. package/dist/scheduler/schedule-store.d.ts +33 -59
  67. package/dist/scheduler/schedule-store.d.ts.map +1 -1
  68. package/dist/scheduler/schedule-tool.d.ts.map +1 -1
  69. package/dist/scheduler/scheduler.d.ts +24 -3
  70. package/dist/scheduler/scheduler.d.ts.map +1 -1
  71. package/dist/scheduler/store/index.d.ts +7 -0
  72. package/dist/scheduler/store/index.d.ts.map +1 -0
  73. package/dist/scheduler/store/json-migrator.d.ts +10 -0
  74. package/dist/scheduler/store/json-migrator.d.ts.map +1 -0
  75. package/dist/scheduler/store/sqlite-store.d.ts +32 -0
  76. package/dist/scheduler/store/sqlite-store.d.ts.map +1 -0
  77. package/dist/scheduler/store/types.d.ts +127 -0
  78. package/dist/scheduler/store/types.d.ts.map +1 -0
  79. package/dist/sdk/check-provider-registry-M3Y6JMTW.mjs +28 -0
  80. package/dist/sdk/check-provider-registry-PANIXYRB.mjs +28 -0
  81. package/dist/sdk/{chunk-D5KI4YQ4.mjs → chunk-DIND4ZCV.mjs} +2 -2
  82. package/dist/sdk/{chunk-DGZPPGJJ.mjs → chunk-EUUAQBTW.mjs} +1463 -568
  83. package/dist/sdk/chunk-EUUAQBTW.mjs.map +1 -0
  84. package/dist/sdk/{chunk-XDLQ3UNF.mjs → chunk-GEW6LS32.mjs} +2 -2
  85. package/dist/sdk/{chunk-N7HO6KKC.mjs → chunk-HOKQOO3G.mjs} +11 -6
  86. package/dist/sdk/chunk-HOKQOO3G.mjs.map +1 -0
  87. package/dist/sdk/{chunk-XR7XXGL7.mjs → chunk-JL7JXCET.mjs} +2 -2
  88. package/dist/sdk/{chunk-6W75IMDC.mjs → chunk-LG4AUKHB.mjs} +2 -2
  89. package/dist/sdk/{chunk-BDGUM6BA.mjs → chunk-S6CD7GFM.mjs} +1463 -568
  90. package/dist/sdk/chunk-S6CD7GFM.mjs.map +1 -0
  91. package/dist/sdk/{chunk-PO7X5XI7.mjs → chunk-SZXICFQ3.mjs} +2 -2
  92. package/dist/sdk/{chunk-HEX3RL32.mjs → chunk-UCMJJ3IM.mjs} +5 -2
  93. package/dist/sdk/{chunk-HEX3RL32.mjs.map → chunk-UCMJJ3IM.mjs.map} +1 -1
  94. package/dist/sdk/{chunk-7YSOINAQ.mjs → chunk-UCNT3PDT.mjs} +342 -5
  95. package/dist/sdk/chunk-UCNT3PDT.mjs.map +1 -0
  96. package/dist/sdk/{chunk-R5Z7YWPB.mjs → chunk-V2IV3ILA.mjs} +7 -5
  97. package/dist/sdk/chunk-V2IV3ILA.mjs.map +1 -0
  98. package/dist/sdk/{chunk-SGS2VMEL.mjs → chunk-VMLORODQ.mjs} +107 -20
  99. package/dist/sdk/chunk-VMLORODQ.mjs.map +1 -0
  100. package/dist/sdk/{chunk-2KB35MB7.mjs → chunk-VPC3QSPW.mjs} +2 -2
  101. package/dist/sdk/{chunk-J5RGJQ53.mjs → chunk-YJRBN3XS.mjs} +2 -2
  102. package/dist/sdk/{command-executor-DVVXERLR.mjs → command-executor-TOYBBE7S.mjs} +4 -4
  103. package/dist/sdk/{config-7VTT64SQ.mjs → config-OGOS4ZU4.mjs} +4 -4
  104. package/dist/sdk/failure-condition-evaluator-HC3M5377.mjs +17 -0
  105. package/dist/sdk/{github-frontend-3N2NLO66.mjs → github-frontend-E2KJSC3Y.mjs} +7 -7
  106. package/dist/sdk/{host-ONVMEHAA.mjs → host-EE6EJ2FM.mjs} +4 -4
  107. package/dist/sdk/lazy-otel-5NH4ZJJM.mjs +24 -0
  108. package/dist/sdk/{liquid-extensions-5IZLTFSZ.mjs → liquid-extensions-E4EUOCES.mjs} +5 -5
  109. package/dist/sdk/memory-store-AAPL2MTE.mjs +12 -0
  110. package/dist/sdk/{metrics-GXQ2EDXA.mjs → metrics-I6A7IHG4.mjs} +3 -3
  111. package/dist/sdk/{prompt-state-YHGXB2OA.mjs → prompt-state-VAKKC773.mjs} +4 -4
  112. package/dist/sdk/{renderer-schema-CMXOLNIG.mjs → renderer-schema-HXEW6BRJ.mjs} +3 -3
  113. package/dist/sdk/{routing-S3Y7T2X3.mjs → routing-OZQWAGAI.mjs} +9 -8
  114. package/dist/sdk/schedule-tool-handler-B7TMSG6A.mjs +38 -0
  115. package/dist/sdk/schedule-tool-handler-IEB2VS7O.mjs +38 -0
  116. package/dist/sdk/sdk.d.mts +134 -4
  117. package/dist/sdk/sdk.d.ts +134 -4
  118. package/dist/sdk/sdk.js +2509 -1085
  119. package/dist/sdk/sdk.js.map +1 -1
  120. package/dist/sdk/sdk.mjs +14 -14
  121. package/dist/sdk/{slack-frontend-R3M2CACB.mjs → slack-frontend-LAY45IBR.mjs} +119 -29
  122. package/dist/sdk/slack-frontend-LAY45IBR.mjs.map +1 -0
  123. package/dist/sdk/{trace-helpers-YHNPC7MR.mjs → trace-helpers-PP3YHTAM.mjs} +3 -3
  124. package/dist/sdk/{tui-frontend-S546M7A7.mjs → tui-frontend-T56PZB67.mjs} +25 -16
  125. package/dist/sdk/tui-frontend-T56PZB67.mjs.map +1 -0
  126. package/dist/sdk/workflow-check-provider-2ET3SFZH.mjs +28 -0
  127. package/dist/sdk/workflow-check-provider-2ET3SFZH.mjs.map +1 -0
  128. package/dist/sdk/workflow-check-provider-HB4XTD4Z.mjs +28 -0
  129. package/dist/sdk/workflow-check-provider-HB4XTD4Z.mjs.map +1 -0
  130. package/dist/sdk/workflow-registry-AAD37XKZ.mjs +12 -0
  131. package/dist/sdk/workflow-registry-AAD37XKZ.mjs.map +1 -0
  132. package/dist/slack/client.d.ts +12 -0
  133. package/dist/slack/client.d.ts.map +1 -1
  134. package/dist/slack/slack-output-adapter.d.ts.map +1 -1
  135. package/dist/slack/socket-runner.d.ts.map +1 -1
  136. package/dist/state-machine/dispatch/execution-invoker.d.ts.map +1 -1
  137. package/dist/state-machine/dispatch/policy-gate.d.ts +28 -0
  138. package/dist/state-machine/dispatch/policy-gate.d.ts.map +1 -0
  139. package/dist/state-machine/states/level-dispatch.d.ts.map +1 -1
  140. package/dist/state-machine/states/routing.d.ts.map +1 -1
  141. package/dist/state-machine/states/wave-planning.d.ts.map +1 -1
  142. package/dist/state-machine-execution-engine.d.ts.map +1 -1
  143. package/dist/test-runner/core/flow-stage.d.ts.map +1 -1
  144. package/dist/test-runner/validator.d.ts.map +1 -1
  145. package/dist/traces/{run-2026-02-08T18-16-04-160Z.ndjson → run-2026-02-11T16-20-59-999Z.ndjson} +84 -84
  146. package/dist/{output/traces/run-2026-02-08T18-16-51-253Z.ndjson → traces/run-2026-02-11T16-21-47-711Z.ndjson} +1032 -1032
  147. package/dist/tui/chat-runner.d.ts.map +1 -1
  148. package/dist/tui/chat-state.d.ts +1 -0
  149. package/dist/tui/chat-state.d.ts.map +1 -1
  150. package/dist/tui/chat-tui.d.ts +3 -2
  151. package/dist/tui/chat-tui.d.ts.map +1 -1
  152. package/dist/tui/components/chat-box.d.ts +9 -0
  153. package/dist/tui/components/chat-box.d.ts.map +1 -1
  154. package/dist/tui/components/input-bar.d.ts +18 -1
  155. package/dist/tui/components/input-bar.d.ts.map +1 -1
  156. package/dist/tui/components/status-bar.d.ts +5 -2
  157. package/dist/tui/components/status-bar.d.ts.map +1 -1
  158. package/dist/tui/components/trace-viewer.d.ts +1 -0
  159. package/dist/tui/components/trace-viewer.d.ts.map +1 -1
  160. package/dist/tui/tui-frontend.d.ts.map +1 -1
  161. package/dist/types/config.d.ts +107 -3
  162. package/dist/types/config.d.ts.map +1 -1
  163. package/dist/types/engine.d.ts +5 -0
  164. package/dist/types/engine.d.ts.map +1 -1
  165. package/dist/types/execution.d.ts +1 -1
  166. package/dist/types/execution.d.ts.map +1 -1
  167. package/package.json +14 -4
  168. package/dist/sdk/check-provider-registry-ACRGIYOB.mjs +0 -28
  169. package/dist/sdk/check-provider-registry-VYHKFHK2.mjs +0 -28
  170. package/dist/sdk/chunk-7YSOINAQ.mjs.map +0 -1
  171. package/dist/sdk/chunk-BDGUM6BA.mjs.map +0 -1
  172. package/dist/sdk/chunk-DGZPPGJJ.mjs.map +0 -1
  173. package/dist/sdk/chunk-N7HO6KKC.mjs.map +0 -1
  174. package/dist/sdk/chunk-R5Z7YWPB.mjs.map +0 -1
  175. package/dist/sdk/chunk-SGS2VMEL.mjs.map +0 -1
  176. package/dist/sdk/failure-condition-evaluator-4WMDF4Q3.mjs +0 -17
  177. package/dist/sdk/memory-store-3N4AZCYB.mjs +0 -12
  178. package/dist/sdk/slack-frontend-R3M2CACB.mjs.map +0 -1
  179. package/dist/sdk/tui-frontend-S546M7A7.mjs.map +0 -1
  180. package/dist/sdk/workflow-check-provider-4F3432ZP.mjs +0 -28
  181. package/dist/sdk/workflow-check-provider-A44PBPG2.mjs +0 -28
  182. package/dist/sdk/workflow-registry-ZAYYXLEP.mjs +0 -12
  183. /package/dist/sdk/{check-provider-registry-ACRGIYOB.mjs.map → check-provider-registry-M3Y6JMTW.mjs.map} +0 -0
  184. /package/dist/sdk/{check-provider-registry-VYHKFHK2.mjs.map → check-provider-registry-PANIXYRB.mjs.map} +0 -0
  185. /package/dist/sdk/{chunk-D5KI4YQ4.mjs.map → chunk-DIND4ZCV.mjs.map} +0 -0
  186. /package/dist/sdk/{chunk-XDLQ3UNF.mjs.map → chunk-GEW6LS32.mjs.map} +0 -0
  187. /package/dist/sdk/{chunk-XR7XXGL7.mjs.map → chunk-JL7JXCET.mjs.map} +0 -0
  188. /package/dist/sdk/{chunk-6W75IMDC.mjs.map → chunk-LG4AUKHB.mjs.map} +0 -0
  189. /package/dist/sdk/{chunk-PO7X5XI7.mjs.map → chunk-SZXICFQ3.mjs.map} +0 -0
  190. /package/dist/sdk/{chunk-2KB35MB7.mjs.map → chunk-VPC3QSPW.mjs.map} +0 -0
  191. /package/dist/sdk/{chunk-J5RGJQ53.mjs.map → chunk-YJRBN3XS.mjs.map} +0 -0
  192. /package/dist/sdk/{command-executor-DVVXERLR.mjs.map → command-executor-TOYBBE7S.mjs.map} +0 -0
  193. /package/dist/sdk/{config-7VTT64SQ.mjs.map → config-OGOS4ZU4.mjs.map} +0 -0
  194. /package/dist/sdk/{failure-condition-evaluator-4WMDF4Q3.mjs.map → failure-condition-evaluator-HC3M5377.mjs.map} +0 -0
  195. /package/dist/sdk/{github-frontend-3N2NLO66.mjs.map → github-frontend-E2KJSC3Y.mjs.map} +0 -0
  196. /package/dist/sdk/{host-ONVMEHAA.mjs.map → host-EE6EJ2FM.mjs.map} +0 -0
  197. /package/dist/sdk/{liquid-extensions-5IZLTFSZ.mjs.map → lazy-otel-5NH4ZJJM.mjs.map} +0 -0
  198. /package/dist/sdk/{memory-store-3N4AZCYB.mjs.map → liquid-extensions-E4EUOCES.mjs.map} +0 -0
  199. /package/dist/sdk/{metrics-GXQ2EDXA.mjs.map → memory-store-AAPL2MTE.mjs.map} +0 -0
  200. /package/dist/sdk/{prompt-state-YHGXB2OA.mjs.map → metrics-I6A7IHG4.mjs.map} +0 -0
  201. /package/dist/sdk/{routing-S3Y7T2X3.mjs.map → prompt-state-VAKKC773.mjs.map} +0 -0
  202. /package/dist/sdk/{renderer-schema-CMXOLNIG.mjs.map → renderer-schema-HXEW6BRJ.mjs.map} +0 -0
  203. /package/dist/sdk/{trace-helpers-YHNPC7MR.mjs.map → routing-OZQWAGAI.mjs.map} +0 -0
  204. /package/dist/sdk/{workflow-check-provider-4F3432ZP.mjs.map → schedule-tool-handler-B7TMSG6A.mjs.map} +0 -0
  205. /package/dist/sdk/{workflow-check-provider-A44PBPG2.mjs.map → schedule-tool-handler-IEB2VS7O.mjs.map} +0 -0
  206. /package/dist/sdk/{workflow-registry-ZAYYXLEP.mjs.map → trace-helpers-PP3YHTAM.mjs.map} +0 -0
@@ -0,0 +1,1325 @@
1
+ # Enterprise Policy Engine (OPA)
2
+
3
+ > **Enterprise Edition feature.** A valid Visor EE license is required.
4
+ > Contact **hello@probelabs.com** for licensing.
5
+
6
+ The OPA (Open Policy Agent) policy engine provides fine-grained, role-based access control over Visor workflows. Policies are written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) and evaluated locally via WebAssembly (WASM) or against a remote OPA server.
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ - [Overview](#overview)
13
+ - [What It Controls](#what-it-controls)
14
+ - [Installation](#installation)
15
+ - [Dependencies](#dependencies)
16
+ - [License Setup](#license-setup)
17
+ - [Configuration Reference](#configuration-reference)
18
+ - [Writing Rego Policies](#writing-rego-policies)
19
+ - [Policy Scopes](#policy-scopes)
20
+ - [Input Document Reference](#input-document-reference)
21
+ - [Per-Step Policy Overrides](#per-step-policy-overrides)
22
+ - [Custom Rule Paths with `policy.rule`](#custom-rule-paths-with-policyrule)
23
+ - [Local WASM Mode](#local-wasm-mode)
24
+ - [Remote OPA Server Mode](#remote-opa-server-mode)
25
+ - [Fallback Behavior](#fallback-behavior)
26
+ - [How It Works](#how-it-works)
27
+ - [Relationship to Author Permissions](#relationship-to-author-permissions)
28
+ - [Troubleshooting](#troubleshooting)
29
+ - [Examples](#examples)
30
+
31
+ ---
32
+
33
+ ## Overview
34
+
35
+ The policy engine sits between your `.visor.yaml` configuration and check execution. Before a check runs, a tool is invoked, or AI capabilities are assembled, the engine evaluates an OPA policy to decide whether the action is allowed.
36
+
37
+ Key properties:
38
+
39
+ - **Deny by default**: Policies can be configured with `fallback: deny` so that any evaluation failure or unrecognized role is blocked.
40
+ - **Role-based**: Roles are resolved from GitHub `author_association`, team slugs, or explicit usernames, then passed into OPA as `input.actor.roles`.
41
+ - **Per-step overrides**: Individual steps can declare `policy.require` and `policy.deny` in YAML without writing any Rego.
42
+ - **Two evaluation backends**: Local WASM (zero network, ~1ms per evaluation) or remote OPA server (shared policy management).
43
+ - **Graceful degradation**: Without a valid license, the engine silently disables and all checks run normally.
44
+
45
+ ---
46
+
47
+ ## What It Controls
48
+
49
+ | Scope | When Evaluated | What It Does |
50
+ |-------|----------------|--------------|
51
+ | **Check execution** (`check.execute`) | Before each check runs | Gate which checks can run based on the actor's role |
52
+ | **MCP tool access** (`tool.invoke`) | Before each MCP tool call | Allow or block specific MCP methods per role |
53
+ | **AI capabilities** (`capability.resolve`) | When assembling AI provider config | Restrict `allowBash`, `allowEdit`, and tool lists per role |
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ### 1. Install the EE build
60
+
61
+ ```bash
62
+ npm install @probelabs/visor@ee
63
+ ```
64
+
65
+ Or as a global tool:
66
+
67
+ ```bash
68
+ npm install -g @probelabs/visor@ee
69
+ ```
70
+
71
+ The EE build is a superset of the OSS build. All OSS functionality works identically. The enterprise code is inert without a license.
72
+
73
+ ### 2. Install OPA CLI (optional, for local compilation)
74
+
75
+ The OPA CLI is needed only if you use `.rego` files with the `local` engine mode. Visor compiles `.rego` to `.wasm` at startup using the `opa` CLI.
76
+
77
+ **macOS (Homebrew):**
78
+ ```bash
79
+ brew install opa
80
+ ```
81
+
82
+ **Linux (binary):**
83
+ ```bash
84
+ curl -L -o /usr/local/bin/opa \
85
+ https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
86
+ chmod +x /usr/local/bin/opa
87
+ ```
88
+
89
+ **Docker:**
90
+ ```bash
91
+ docker pull openpolicyagent/opa:latest
92
+ ```
93
+
94
+ **Verify installation:**
95
+ ```bash
96
+ opa version
97
+ # Expected: Version: 0.70.0 or later
98
+ ```
99
+
100
+ > **Note**: If you pre-compile your `.rego` files into a `.wasm` bundle (see [Pre-compiling WASM bundles](#pre-compiling-wasm-bundles)), the OPA CLI is not needed at runtime.
101
+
102
+ ### 3. Install the WASM runtime (automatic)
103
+
104
+ The `@open-policy-agent/opa-wasm` npm package is an optional dependency of the EE build. It is installed automatically when you install `@probelabs/visor@ee`. If for some reason it's missing:
105
+
106
+ ```bash
107
+ npm install @open-policy-agent/opa-wasm
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Dependencies
113
+
114
+ | Dependency | Required? | Purpose |
115
+ |-----------|-----------|---------|
116
+ | `@probelabs/visor@ee` | Yes | Visor Enterprise Edition build |
117
+ | Valid EE license (JWT) | Yes | Activates the policy engine |
118
+ | `opa` CLI | Only for `local` mode with `.rego` files | Compiles Rego to WASM at startup |
119
+ | `@open-policy-agent/opa-wasm` | Only for `local` mode | Evaluates WASM policies in-process |
120
+ | OPA server | Only for `remote` mode | External policy evaluation via HTTP |
121
+
122
+ ### Rego language
123
+
124
+ Rego is OPA's declarative policy language. Key resources:
125
+
126
+ - [Rego language reference](https://www.openpolicyagent.org/docs/latest/policy-language/)
127
+ - [Rego playground](https://play.openpolicyagent.org/) (interactive editor and tester)
128
+ - [OPA documentation](https://www.openpolicyagent.org/docs/latest/)
129
+ - [Rego style guide](https://www.openpolicyagent.org/docs/latest/policy-language/#style-guide)
130
+
131
+ ---
132
+
133
+ ## License Setup
134
+
135
+ The policy engine requires a valid Visor EE license (a JWT signed by ProbeLabs). Visor looks for the license in this order:
136
+
137
+ 1. **`VISOR_LICENSE` environment variable** (the JWT string directly)
138
+ 2. **`VISOR_LICENSE_FILE` environment variable** (path to a file containing the JWT)
139
+ 3. **`.visor-license` file in the project root**
140
+ 4. **`~/.config/visor/.visor-license`** (user-level default)
141
+
142
+ ### Setting up in CI (GitHub Actions)
143
+
144
+ ```yaml
145
+ # .github/workflows/visor.yml
146
+ - uses: probelabs/visor@v1
147
+ env:
148
+ VISOR_LICENSE: ${{ secrets.VISOR_LICENSE }}
149
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
150
+ ```
151
+
152
+ ### Setting up locally
153
+
154
+ ```bash
155
+ # Option A: environment variable
156
+ export VISOR_LICENSE="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..."
157
+
158
+ # Option B: file in project root
159
+ echo "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..." > .visor-license
160
+
161
+ # Option C: user-level config
162
+ mkdir -p ~/.config/visor
163
+ echo "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..." > ~/.config/visor/.visor-license
164
+ ```
165
+
166
+ > **Important**: Add `.visor-license` to your `.gitignore` to avoid committing your license key.
167
+
168
+ ### License features
169
+
170
+ Your license JWT encodes which features are available. The policy engine requires the `policy` feature. If your license doesn't include this feature, the engine falls back to the default (all-allow) behavior.
171
+
172
+ ### Grace period
173
+
174
+ When a license expires, Visor provides a **72-hour grace period** during which the policy engine continues to work. A warning is logged:
175
+
176
+ ```
177
+ [visor:enterprise] License has expired but is within the 72-hour grace period.
178
+ Please renew your license.
179
+ ```
180
+
181
+ After the grace period, the policy engine silently disables.
182
+
183
+ ---
184
+
185
+ ## Configuration Reference
186
+
187
+ ### Top-level `policy:` block
188
+
189
+ Add a `policy:` block to your `.visor.yaml`:
190
+
191
+ ```yaml
192
+ version: "1.0"
193
+
194
+ policy:
195
+ engine: local
196
+ rules: ./policies/
197
+ fallback: deny
198
+ timeout: 5000
199
+
200
+ roles:
201
+ admin:
202
+ author_association: [OWNER]
203
+ users: [cto-username]
204
+ developer:
205
+ author_association: [MEMBER, COLLABORATOR]
206
+ external:
207
+ author_association: [FIRST_TIME_CONTRIBUTOR, FIRST_TIMER, NONE]
208
+ ```
209
+
210
+ ### Field reference
211
+
212
+ | Field | Type | Default | Description |
213
+ |-------|------|---------|-------------|
214
+ | `engine` | `local` \| `remote` \| `disabled` | `disabled` | Evaluation backend |
215
+ | `rules` | `string` \| `string[]` | — | Path to `.rego` files, a directory, or a `.wasm` bundle (local mode only) |
216
+ | `data` | `string` | — | Path to a JSON file to load as the OPA data document (local mode only). Contents are available in Rego via `data.<key>`. |
217
+ | `url` | `string` | — | OPA server URL (remote mode only) |
218
+ | `fallback` | `allow` \| `deny` \| `warn` | `deny` | Default decision when policy evaluation fails or times out. `warn` enables audit mode: violations are logged but checks are not blocked. |
219
+ | `timeout` | `number` | `5000` | Evaluation timeout in milliseconds |
220
+ | `roles` | `map` | — | Role definitions (see below) |
221
+
222
+ ### Role definitions
223
+
224
+ Roles map GitHub metadata to named roles that your Rego policies reference via `input.actor.roles`.
225
+
226
+ ```yaml
227
+ roles:
228
+ admin:
229
+ author_association: [OWNER] # GitHub author associations
230
+ users: [alice, bob] # Explicit GitHub usernames
231
+ teams: [platform-team] # GitHub team slugs (requires API access)
232
+ developer:
233
+ author_association: [MEMBER, COLLABORATOR]
234
+ external:
235
+ author_association: [FIRST_TIME_CONTRIBUTOR, NONE]
236
+ ```
237
+
238
+ | Sub-field | Type | Description |
239
+ |-----------|------|-------------|
240
+ | `author_association` | `string[]` | GitHub author association values: `OWNER`, `MEMBER`, `COLLABORATOR`, `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, `FIRST_TIMER`, `NONE` |
241
+ | `users` | `string[]` | Explicit GitHub usernames |
242
+ | `teams` | `string[]` | GitHub team slugs (reserved for future use; see note below) |
243
+
244
+ > **Note**: The `teams` field is reserved for future use and is **not currently enforced**. Team-based role resolution (matching GitHub team slugs via the GitHub API) is not yet implemented. If you configure `teams`, a validation warning will be emitted. Only `author_association` and `users` are currently used for role resolution.
245
+
246
+ A user is assigned a role if they match **any** of the identity criteria (OR logic). A user can have **multiple roles**.
247
+
248
+ ### Slack identity fields
249
+
250
+ When Visor runs via Slack (socket mode), the actor's Slack user ID, email, and channel are available for role resolution. Three additional sub-fields are supported in role definitions:
251
+
252
+ ```yaml
253
+ roles:
254
+ admin:
255
+ author_association: [OWNER]
256
+ users: [cto-username]
257
+ slack_users: [U0123ADMIN] # Slack user IDs
258
+ emails: [admin@company.com] # Email addresses (case-insensitive)
259
+ eng-channel:
260
+ slack_channels: [C0123ENG] # Channel gate: role only applies from this channel
261
+ slack_users: [U0123ALICE, U0123BOB]
262
+ ```
263
+
264
+ | Sub-field | Type | Description |
265
+ |-----------|------|-------------|
266
+ | `slack_users` | `string[]` | Slack user IDs (e.g., `U0123ABC`). Matched against the triggering Slack user. |
267
+ | `emails` | `string[]` | Email addresses. Matched case-insensitively against the Slack user's email. Requires the Slack bot to have the `users:read.email` OAuth scope. |
268
+ | `slack_channels` | `string[]` | Slack channel IDs (e.g., `C0123ENG`). Acts as a **gate**: the role only applies when the action is triggered from one of these channels. Applied as an AND with any identity match. |
269
+
270
+ **Identity matching** (`users`, `slack_users`, `emails`) is OR — matching any one is sufficient. **Channel gating** (`slack_channels`) is AND — if set on a role, the role only applies when the trigger comes from one of the listed channels.
271
+
272
+ When Visor runs outside of Slack (e.g., GitHub Actions, CLI), the Slack fields are simply not present and roles that only define Slack criteria will not match.
273
+
274
+ ---
275
+
276
+ ## Writing Rego Policies
277
+
278
+ ### Directory structure
279
+
280
+ Create a `policies/` directory (or any name you choose) with `.rego` files:
281
+
282
+ ```
283
+ your-project/
284
+ .visor.yaml
285
+ policies/
286
+ check_execute.rego # Check execution gating (default scope)
287
+ tool_invoke.rego # MCP tool access control
288
+ capability_resolve.rego # AI capability restrictions
289
+ deploy_production.rego # Custom rule for production deploys (optional)
290
+ ```
291
+
292
+ ### Basic policy structure
293
+
294
+ Every policy file:
295
+ 1. Declares a `package` matching the scope (e.g., `package visor.check.execute`)
296
+ 2. Exports an `allowed` boolean (for `check.execute` and `tool.invoke` scopes)
297
+ 3. Optionally exports a `reason` string for denial messages
298
+ 4. Optionally exports a `capabilities` object (for `capability.resolve` scope)
299
+
300
+ ```rego
301
+ package visor.check.execute
302
+
303
+ # Default: deny everything
304
+ default allowed = false
305
+
306
+ # Admin can run anything
307
+ allowed {
308
+ input.actor.roles[_] == "admin"
309
+ }
310
+
311
+ # Developers can run non-production deployments
312
+ allowed {
313
+ input.actor.roles[_] == "developer"
314
+ not startswith(input.check.id, "deploy-production")
315
+ }
316
+
317
+ # Provide a reason when denied
318
+ reason = "insufficient role for this check" { not allowed }
319
+ ```
320
+
321
+ ### Rego tips for Visor
322
+
323
+ **Iterating over roles**: Use `input.actor.roles[_]` to check if any role matches:
324
+ ```rego
325
+ # Any of the actor's roles is "admin"
326
+ allowed {
327
+ input.actor.roles[_] == "admin"
328
+ }
329
+ ```
330
+
331
+ **Per-step YAML deny list**: When a step declares `policy.deny`, use a `denied` helper to block matching roles. Add `not denied` to every `allowed` rule so deny always takes precedence:
332
+ ```rego
333
+ # Deny takes precedence — any actor role in the deny list blocks the check
334
+ denied {
335
+ some i, j
336
+ input.check.policy.deny[i] == input.actor.roles[j]
337
+ }
338
+
339
+ # All allowed rules must include `not denied`
340
+ allowed {
341
+ not denied
342
+ input.actor.roles[_] == "admin"
343
+ }
344
+ ```
345
+
346
+ **Per-step YAML requirements**: When a step declares `policy.require`, check it in Rego:
347
+ ```rego
348
+ # String require (e.g., require: admin)
349
+ allowed {
350
+ not denied
351
+ required := input.check.policy.require
352
+ is_string(required)
353
+ input.actor.roles[_] == required
354
+ }
355
+
356
+ # Array require (e.g., require: [developer, admin])
357
+ allowed {
358
+ not denied
359
+ required := input.check.policy.require
360
+ is_array(required)
361
+ required[_] == input.actor.roles[_]
362
+ }
363
+ ```
364
+
365
+ **Local mode bypass**: Allow broader access when running locally, but only for checks that have no explicit `policy.require` setting. This keeps sensitive steps (e.g. production deployments) protected even during local development:
366
+ ```rego
367
+ # Checks WITHOUT policy.require are allowed in local mode (convenience).
368
+ # Checks WITH policy.require still enforce roles (security).
369
+ allowed {
370
+ input.actor.isLocalMode == true
371
+ not input.check.policy
372
+ }
373
+ ```
374
+
375
+ **WASM compilation safety**: Some Rego patterns are not supported by OPA's WASM compiler. Avoid `not set[_] == X` — use helper rules instead:
376
+
377
+ ```rego
378
+ # BAD: unsafe for WASM compilation
379
+ allowed = false {
380
+ not input.actor.roles[_] == "admin"
381
+ }
382
+
383
+ # GOOD: use a helper rule
384
+ is_admin { input.actor.roles[_] == "admin" }
385
+ allowed = false {
386
+ not is_admin
387
+ }
388
+ ```
389
+
390
+ ### Testing policies locally
391
+
392
+ Use the OPA CLI to test your policies before deploying:
393
+
394
+ ```bash
395
+ # Evaluate a policy with test input
396
+ echo '{"actor":{"roles":["developer"],"isLocalMode":false},"check":{"id":"deploy-staging"}}' | \
397
+ opa eval -d policies/ -i /dev/stdin 'data.visor.check.execute.allowed'
398
+
399
+ # Run OPA unit tests (if you have _test.rego files)
400
+ opa test policies/ -v
401
+ ```
402
+
403
+ ### Validating policies with `visor policy-check`
404
+
405
+ Visor includes a built-in policy validation command that checks your `.rego` files for syntax errors and WASM compilation compatibility in one step. This command does **not** require a license.
406
+
407
+ ```bash
408
+ # Validate a directory of .rego files
409
+ visor policy-check ./policies/
410
+
411
+ # Validate a single .rego file
412
+ visor policy-check ./policies/check_execute.rego
413
+
414
+ # Use the policy.rules path from .visor.yaml automatically
415
+ visor policy-check
416
+
417
+ # Use a specific config file
418
+ visor policy-check --config .visor.yaml
419
+
420
+ # Validate and evaluate against sample input
421
+ visor policy-check ./policies/ --input sample-input.json
422
+
423
+ # Verbose output (shows the exact opa commands being run)
424
+ visor policy-check ./policies/ --verbose
425
+ ```
426
+
427
+ The command performs three checks:
428
+
429
+ 1. **Syntax validation** (`opa check`): Verifies each `.rego` file has valid Rego syntax.
430
+ 2. **WASM compilation** (`opa build -t wasm -e visor`): Confirms the policies can be compiled to WebAssembly, catching WASM-unsafe patterns early.
431
+ 3. **Sample evaluation** (optional, `--input`): Evaluates all three policy scopes (`check.execute`, `tool.invoke`, `capability.resolve`) against a provided JSON input file.
432
+
433
+ Example sample input file for testing:
434
+
435
+ ```json
436
+ {
437
+ "actor": {
438
+ "login": "alice",
439
+ "authorAssociation": "MEMBER",
440
+ "roles": ["developer"],
441
+ "isLocalMode": false
442
+ },
443
+ "check": {
444
+ "id": "security-scan",
445
+ "type": "ai",
446
+ "policy": {
447
+ "require": "developer"
448
+ }
449
+ },
450
+ "repository": {
451
+ "owner": "myorg",
452
+ "name": "myrepo",
453
+ "branch": "feat/new-feature",
454
+ "baseBranch": "main",
455
+ "event": "pull_request"
456
+ },
457
+ "pullRequest": {}
458
+ }
459
+ ```
460
+
461
+ > **Note**: The `pullRequest` object is shown as empty because those fields are not currently populated by the policy engine. See the [Input Document Reference](#input-document-reference) for details.
462
+
463
+ Exit codes:
464
+ - `0`: All checks passed
465
+ - `1`: One or more checks failed (syntax errors, WASM compilation failure, or missing files)
466
+
467
+ ### Pre-compiling WASM bundles
468
+
469
+ For faster startup (skip compilation at runtime), pre-compile your policies:
470
+
471
+ ```bash
472
+ # Compile all .rego files into a WASM bundle
473
+ opa build -t wasm -e visor -d policies/ -o bundle.tar.gz
474
+
475
+ # Extract the WASM file
476
+ tar -xzf bundle.tar.gz /policy.wasm
477
+
478
+ # Reference the .wasm file in config
479
+ # policy:
480
+ # rules: ./policy.wasm
481
+ ```
482
+
483
+ > **Important**: When compiling with `opa build`, always use `-e visor` as the entrypoint. Visor navigates the WASM result tree starting from the `visor` package root.
484
+
485
+ ---
486
+
487
+ ## Policy Scopes
488
+
489
+ ### `check.execute` — Check execution gating
490
+
491
+ **When**: Before each check runs, after `if` condition evaluation
492
+ **Package**: `package visor.check.execute`
493
+ **Decision**: `allowed` (boolean), `reason` (string)
494
+
495
+ ```rego
496
+ package visor.check.execute
497
+
498
+ default allowed = false
499
+
500
+ # Deny list from YAML policy.deny (see Per-Step Policy Overrides)
501
+ denied {
502
+ some i, j
503
+ input.check.policy.deny[i] == input.actor.roles[j]
504
+ }
505
+
506
+ allowed {
507
+ not denied
508
+ input.actor.roles[_] == "admin"
509
+ }
510
+
511
+ allowed {
512
+ not denied
513
+ input.actor.roles[_] == "developer"
514
+ not startswith(input.check.id, "deploy-production")
515
+ }
516
+
517
+ reason = "role is in the deny list for this check" { denied }
518
+ reason = "insufficient role for this check" { not denied; not allowed }
519
+ ```
520
+
521
+ When a check is denied, it is skipped with `skipReason: policy_denied`. The denial reason appears in the execution stats and JSON output.
522
+
523
+ ### `tool.invoke` — MCP tool access control
524
+
525
+ **When**: Before each MCP tool/method call
526
+ **Package**: `package visor.tool.invoke`
527
+ **Decision**: `allowed` (boolean), `reason` (string)
528
+
529
+ ```rego
530
+ package visor.tool.invoke
531
+
532
+ default allowed = true
533
+
534
+ # Block destructive methods for non-admins
535
+ allowed = false {
536
+ endswith(input.tool.methodName, "_delete")
537
+ not is_admin
538
+ }
539
+
540
+ is_admin { input.actor.roles[_] == "admin" }
541
+
542
+ reason = "tool access denied by policy" { not allowed }
543
+ ```
544
+
545
+ This scope works as an overlay on top of the static `allowedMethods`/`blockedMethods` configuration in `McpServerConfig`. Static filtering is applied first, then OPA filtering.
546
+
547
+ ### `capability.resolve` — AI capability restrictions
548
+
549
+ **When**: When assembling AI provider configuration
550
+ **Package**: `package visor.capability.resolve`
551
+ **Decision**: `capabilities` (object with `allowEdit`, `allowBash`, `allowedTools` keys)
552
+
553
+ ```rego
554
+ package visor.capability.resolve
555
+
556
+ # Helper rules for WASM-safe role checks
557
+ is_developer { input.actor.roles[_] == "developer" }
558
+ is_admin { input.actor.roles[_] == "admin" }
559
+
560
+ # Disable file editing for non-developers
561
+ capabilities["allowEdit"] = false {
562
+ not is_developer
563
+ not is_admin
564
+ }
565
+
566
+ # Disable bash for external contributors
567
+ capabilities["allowBash"] = false {
568
+ input.actor.roles[_] == "external"
569
+ }
570
+ ```
571
+
572
+ Returned capability restrictions are merged with the YAML configuration. OPA can only **restrict** capabilities (set to `false` or reduce `allowedTools`), never grant more than the YAML config allows.
573
+
574
+ ---
575
+
576
+ ## Input Document Reference
577
+
578
+ Your Rego policies receive an `input` document with these fields:
579
+
580
+ ```json
581
+ {
582
+ "scope": "check.execute",
583
+ "check": {
584
+ "id": "deploy-production",
585
+ "type": "command",
586
+ "group": "deployment",
587
+ "tags": ["deploy", "production"],
588
+ "criticality": "external",
589
+ "sandbox": "docker-image",
590
+ "policy": {
591
+ "require": "admin",
592
+ "deny": ["external"],
593
+ "rule": "visor/deploy/production"
594
+ }
595
+ },
596
+ "tool": {
597
+ "serverName": "github",
598
+ "methodName": "search_repositories",
599
+ "transport": "stdio"
600
+ },
601
+ "capability": {
602
+ "allowEdit": true,
603
+ "allowBash": true,
604
+ "allowedTools": ["search_*"],
605
+ "enableDelegate": false,
606
+ "sandbox": "docker-image"
607
+ },
608
+ "actor": {
609
+ "login": "alice",
610
+ "authorAssociation": "MEMBER",
611
+ "roles": ["developer"],
612
+ "isLocalMode": false,
613
+ "slack": {
614
+ "userId": "U0123ALICE",
615
+ "email": "alice@company.com",
616
+ "channelId": "C0123ENG",
617
+ "channelType": "channel"
618
+ }
619
+ },
620
+ "repository": {
621
+ "owner": "probelabs",
622
+ "name": "visor",
623
+ "branch": "feat/new-feature",
624
+ "baseBranch": "main",
625
+ "event": "pull_request"
626
+ // "action": "synchronize" // Not currently populated
627
+ },
628
+ "pullRequest": {
629
+ // The following fields are not currently populated:
630
+ // "number": 42,
631
+ // "labels": ["approved", "ready-to-merge"],
632
+ // "draft": false,
633
+ // "changedFiles": 5
634
+ }
635
+ }
636
+ ```
637
+
638
+ > **Note**: Only the fields relevant to each scope are populated. For example, `check` is populated for `check.execute`, `tool` is populated for `tool.invoke`, etc.
639
+ >
640
+ > **Important**: Several fields in the `repository` and `pullRequest` objects are currently **not populated** and are reserved for future use:
641
+ > - `repository.action` - Would contain the GitHub event action (e.g., "synchronize", "opened")
642
+ > - `pullRequest.number`, `pullRequest.labels`, `pullRequest.draft`, `pullRequest.changedFiles` - Would contain PR metadata
643
+ >
644
+ > These fields are defined in the input schema but are not currently set by the policy engine during initialization. They are documented here for future compatibility. The `OpaPolicyEngine` class has a `setActorContext()` method that could be used to enrich this context after PR data is fetched, but this is not yet implemented in the main codebase.
645
+ >
646
+ > For now, use the `repository.owner`, `repository.name`, `repository.branch`, `repository.baseBranch`, and `repository.event` fields, along with `actor` fields, which are reliably populated from GitHub Actions environment variables.
647
+
648
+ ### Field descriptions
649
+
650
+ | Path | Type | Description |
651
+ |------|------|-------------|
652
+ | `scope` | string | The policy scope being evaluated |
653
+ | `check.id` | string | Step/check ID from `.visor.yaml` |
654
+ | `check.type` | string | Provider type (`ai`, `command`, `mcp`, etc.) |
655
+ | `check.group` | string | Comment group name |
656
+ | `check.tags` | string[] | Tags assigned to the check |
657
+ | `check.criticality` | string | `external`, `internal`, `policy`, or `info` |
658
+ | `check.sandbox` | string | Sandbox type if configured |
659
+ | `check.policy` | object | Per-step policy override from YAML |
660
+ | `tool.serverName` | string | MCP server name |
661
+ | `tool.methodName` | string | MCP method being invoked |
662
+ | `tool.transport` | string | MCP transport type (`stdio`, `sse`, `http`) |
663
+ | `actor.login` | string | GitHub username |
664
+ | `actor.authorAssociation` | string | Raw GitHub author association |
665
+ | `actor.roles` | string[] | Resolved roles from `policy.roles` config |
666
+ | `actor.isLocalMode` | boolean | `true` when running outside GitHub Actions |
667
+ | `actor.slack.userId` | string | Slack user ID (e.g., `U0123ABC`). Present only when triggered from Slack. |
668
+ | `actor.slack.email` | string | Slack user's email address. Requires the bot's `users:read.email` OAuth scope. |
669
+ | `actor.slack.channelId` | string | Slack channel ID where the action was triggered (e.g., `C0123ENG`). |
670
+ | `actor.slack.channelType` | string | Channel type: `channel`, `dm`, or `group`. |
671
+ | `repository.owner` | string | Repository owner/organization (from `GITHUB_REPOSITORY_OWNER`) |
672
+ | `repository.name` | string | Repository name (from `GITHUB_REPOSITORY`) |
673
+ | `repository.branch` | string | Current/head branch (from `GITHUB_HEAD_REF`) |
674
+ | `repository.baseBranch` | string | Base branch for PRs (from `GITHUB_BASE_REF`) |
675
+ | `repository.event` | string | GitHub event type (from `GITHUB_EVENT_NAME`) |
676
+ | `repository.action` | string | **Not currently populated.** Reserved for future use. |
677
+ | `pullRequest.number` | number | **Not currently populated.** Would require a `GITHUB_PR_NUMBER` env var, which is a custom variable that must be set manually (it is not provided by GitHub Actions by default). When running as a GitHub Action, PR context is also enriched automatically from the PR info, but this field is not yet wired up. Reserved for future use. |
678
+ | `pullRequest.labels` | string[] | **Not currently populated.** Reserved for future use (requires PR data enrichment). |
679
+ | `pullRequest.draft` | boolean | **Not currently populated.** Reserved for future use (requires PR data enrichment). |
680
+ | `pullRequest.changedFiles` | number | **Not currently populated.** Reserved for future use (requires PR data enrichment). |
681
+
682
+ ---
683
+
684
+ ## Per-Step Policy Overrides
685
+
686
+ Individual steps can declare policy requirements directly in YAML. This is a convenience shortcut that works without writing Rego (though your Rego must handle the `input.check.policy` field for it to take effect).
687
+
688
+ ```yaml
689
+ checks:
690
+ deploy-staging:
691
+ type: command
692
+ exec: ./deploy.sh staging
693
+ policy:
694
+ require: [developer, admin] # Any of these roles can run this step
695
+ deny: [external] # These roles are explicitly blocked
696
+ rule: visor/deploy/staging # Optional: custom OPA rule path
697
+ ```
698
+
699
+ > **Note**: `steps:` is a supported alias for `checks:` for backward compatibility. This documentation uses `checks:` as the primary key.
700
+
701
+ | Field | Type | Description |
702
+ |-------|------|-------------|
703
+ | `require` | `string` \| `string[]` | Role(s) required to run the step (any match suffices) |
704
+ | `deny` | `string[]` | Role(s) explicitly denied from running the step (deny takes precedence over allow) |
705
+ | `rule` | `string` | Custom OPA rule path (overrides the default scope-based path) |
706
+
707
+ ### How `deny` works
708
+
709
+ The `deny` field is an explicit blocklist. If any of the actor's resolved roles appear in the `deny` array, the check is **unconditionally blocked** -- even if the actor also has a role that would otherwise satisfy `require` or a hardcoded `allowed` rule. Deny always takes precedence over allow.
710
+
711
+ **Example**: A user with both `developer` and `external` roles attempts to run this step:
712
+
713
+ ```yaml
714
+ deploy-staging:
715
+ type: command
716
+ exec: ./deploy.sh staging
717
+ policy:
718
+ require: [developer, admin]
719
+ deny: [external]
720
+ ```
721
+
722
+ Even though the user has the `developer` role (which satisfies `require`), the step is blocked because they also have the `external` role, which appears in `deny`.
723
+
724
+ **Rego implementation**: The `deny` field is passed to OPA as `input.check.policy.deny`. Your Rego rules must include a `denied` helper that checks this field. The example policy in [`examples/enterprise-policy/policies/check_execute.rego`](../examples/enterprise-policy/policies/check_execute.rego) includes this enforcement:
725
+
726
+ ```rego
727
+ # Explicit deny list from YAML policy.deny
728
+ denied {
729
+ some i, j
730
+ input.check.policy.deny[i] == input.actor.roles[j]
731
+ }
732
+
733
+ # Every allowed rule includes `not denied` so deny takes precedence
734
+ allowed {
735
+ not denied
736
+ input.actor.roles[_] == "admin"
737
+ }
738
+ ```
739
+
740
+ > **Important**: The `deny` field only takes effect if your Rego policy reads `input.check.policy.deny` and uses it to block the `allowed` decision. The YAML field alone does nothing -- it is data that your policy must act on. The example policies shipped with Visor include this enforcement out of the box.
741
+
742
+ When a check is denied via `policy.deny`, the denial reason will be `"role is in the deny list for this check"` (distinct from the generic `"insufficient role for this check"` reason).
743
+
744
+ ### Custom Rule Paths with `policy.rule`
745
+
746
+ By default, all check execution policies are evaluated against the `visor/check/execute` rule path (corresponding to `package visor.check.execute` in Rego). The `policy.rule` field lets you override this default and route a specific check to a completely different Rego package with its own specialized logic.
747
+
748
+ #### When to use it
749
+
750
+ Use a custom rule path when a check needs specialized policy logic that differs from the general `check.execute` rules. Common scenarios include:
751
+
752
+ - **Production deployments** that require additional safeguards beyond role checks (e.g., branch restrictions, time-of-day controls)
753
+ - **Sensitive operations** that need a dedicated approval workflow
754
+ - **Environment-specific gates** where staging and production have different policy requirements
755
+ - **Compliance checks** that must enforce domain-specific regulations
756
+
757
+ #### How it works
758
+
759
+ When a check declares `policy.rule`, the engine's `resolveRulePath` method returns the custom path instead of the default `visor/check/execute`. For WASM evaluation, the engine navigates the compiled result tree using the custom path segments. For remote OPA, the path is sent as the HTTP endpoint.
760
+
761
+ The flow is:
762
+
763
+ 1. Check config has `policy.rule: visor/deploy/production`
764
+ 2. Engine calls `resolveRulePath('check.execute', 'visor/deploy/production')`
765
+ 3. The override is returned as-is: `visor/deploy/production`
766
+ 4. For WASM: the engine strips the `visor/` prefix and navigates `result.deploy.production`
767
+ 5. For remote OPA: a POST is sent to `${url}/v1/data/visor/deploy/production`
768
+
769
+ #### Complete example
770
+
771
+ **Step 1**: Declare the custom rule in your `.visor.yaml`:
772
+
773
+ ```yaml
774
+ checks:
775
+ deploy-production:
776
+ type: command
777
+ exec: ./deploy.sh production
778
+ criticality: external
779
+ policy:
780
+ require: admin
781
+ rule: visor/deploy/production
782
+ ```
783
+
784
+ **Step 2**: Create the corresponding Rego file. The package name must match the rule path, with slashes converted to dots:
785
+
786
+ ```rego
787
+ # policies/deploy_production.rego
788
+ package visor.deploy.production
789
+
790
+ default allowed = false
791
+
792
+ # Helper: check if actor is an admin (WASM-safe pattern)
793
+ is_admin { input.actor.roles[_] == "admin" }
794
+
795
+ # Only admins can deploy to production
796
+ allowed {
797
+ is_admin
798
+ }
799
+
800
+ # Additionally require the PR to target the main branch
801
+ allowed {
802
+ is_admin
803
+ input.repository.baseBranch == "main"
804
+ }
805
+
806
+ reason = "only admins can deploy to production" { not allowed }
807
+ ```
808
+
809
+ **Step 3**: Place the file in the same directory as your other policies (the directory referenced by `policy.rules` in your top-level config). OPA compiles all `.rego` files in that directory together, so the custom package is automatically included.
810
+
811
+ #### Important notes
812
+
813
+ - **Package name must match the rule path**: The Rego `package` declaration uses dots as separators, while the YAML `rule` field uses slashes. They must correspond: `visor/deploy/production` in YAML maps to `package visor.deploy.production` in Rego.
814
+ - **The `visor/` prefix is recommended**: If omitted, Visor will auto-prepend it (e.g., `deploy/production` becomes `visor/deploy/production`). Visor compiles WASM bundles with `-e visor` as the entrypoint, so all rule paths must ultimately start with `visor/` for the engine to navigate the result tree correctly.
815
+ - **Custom Rego files go in the policy directory**: The file must be in the same directory (or listed in the same `policy.rules` array) as your other `.rego` files. OPA compiles all files together into one WASM bundle.
816
+ - **Custom rules must define `allowed`**: Like the default scopes, custom rules must export an `allowed` boolean. Optionally export a `reason` string for denial messages.
817
+ - **The full input document is available**: Custom rules receive the same `input` document as the default `check.execute` scope, including `input.actor`, `input.repository`, and `input.check` (with the `policy` sub-object containing `require`, `deny`, and `rule`).
818
+ - **Only one rule per check**: Each check can specify at most one `policy.rule`. If omitted, the default `visor/check/execute` path is used.
819
+
820
+ #### Testing custom rules
821
+
822
+ Use the OPA CLI to test your custom rule in isolation:
823
+
824
+ ```bash
825
+ # Test the custom deploy rule with an admin actor
826
+ echo '{
827
+ "actor": {"roles": ["admin"], "isLocalMode": false},
828
+ "check": {"id": "deploy-production", "type": "command"},
829
+ "repository": {"baseBranch": "main"}
830
+ }' | opa eval -d policies/ -i /dev/stdin 'data.visor.deploy.production.allowed'
831
+
832
+ # Expected output: true
833
+
834
+ # Test with a non-admin actor (should be denied)
835
+ echo '{
836
+ "actor": {"roles": ["developer"], "isLocalMode": false},
837
+ "check": {"id": "deploy-production", "type": "command"},
838
+ "repository": {"baseBranch": "main"}
839
+ }' | opa eval -d policies/ -i /dev/stdin 'data.visor.deploy.production.allowed'
840
+
841
+ # Expected output: false
842
+
843
+ # Check the denial reason
844
+ echo '{
845
+ "actor": {"roles": ["developer"], "isLocalMode": false},
846
+ "check": {"id": "deploy-production", "type": "command"},
847
+ "repository": {"baseBranch": "main"}
848
+ }' | opa eval -d policies/ -i /dev/stdin 'data.visor.deploy.production.reason'
849
+
850
+ # Expected output: "only admins can deploy to production"
851
+ ```
852
+
853
+ See [`examples/enterprise-policy/policies/deploy_production.rego`](../examples/enterprise-policy/policies/deploy_production.rego) for the full working example.
854
+
855
+ ---
856
+
857
+ ## Local WASM Mode
858
+
859
+ Local mode compiles Rego policies into WebAssembly and evaluates them in-process. This is the recommended mode for most deployments.
860
+
861
+ ```yaml
862
+ policy:
863
+ engine: local
864
+ rules: ./policies/ # Directory of .rego files
865
+ fallback: deny
866
+ ```
867
+
868
+ ### How it works
869
+
870
+ 1. At startup, Visor finds all `.rego` files in the specified path
871
+ 2. It compiles them to WASM using `opa build -t wasm -e visor`
872
+ 3. The WASM module is loaded into the Node.js process via `@open-policy-agent/opa-wasm`
873
+ 4. Each policy evaluation takes ~1ms (no network round-trip)
874
+
875
+ ### Supported `rules` values
876
+
877
+ | Value | Example | Description |
878
+ |-------|---------|-------------|
879
+ | Directory | `./policies/` | All `.rego` files in the directory are compiled together |
880
+ | Single file | `./policies/main.rego` | A single `.rego` file |
881
+ | Multiple files | `[./policies/check.rego, ./policies/tool.rego]` | Array of `.rego` files |
882
+ | WASM bundle | `./policy.wasm` | Pre-compiled WASM (skips `opa build` at startup) |
883
+
884
+ ### External data document
885
+
886
+ You can load an external JSON file as the OPA data document using the `data` option. This makes the file's contents available in your Rego policies via `data.<key>`, allowing you to externalize dynamic configuration (allowed lists, thresholds, feature flags, etc.) without modifying your `.rego` files.
887
+
888
+ ```yaml
889
+ policy:
890
+ engine: local
891
+ rules: ./policies/
892
+ data: ./policies/data.json
893
+ ```
894
+
895
+ The JSON file must contain a top-level object. For example:
896
+
897
+ ```json
898
+ {
899
+ "allowed_repos": ["visor", "probe"],
900
+ "protected_checks": {
901
+ "deploy-production": true,
902
+ "deploy-staging": true
903
+ },
904
+ "max_concurrent_deploys": 3
905
+ }
906
+ ```
907
+
908
+ You can then reference these values in your Rego policies:
909
+
910
+ ```rego
911
+ package visor.check.execute
912
+
913
+ # Use external data for dynamic configuration
914
+ allowed {
915
+ data.protected_checks[input.check.id]
916
+ input.actor.roles[_] == "admin"
917
+ }
918
+
919
+ # Non-protected checks are allowed for developers
920
+ allowed {
921
+ not data.protected_checks[input.check.id]
922
+ input.actor.roles[_] == "developer"
923
+ }
924
+ ```
925
+
926
+ > **Note**: The `data` option is only supported in `local` mode. For `remote` mode, load data directly into your OPA server using OPA's bundle or data APIs.
927
+
928
+ ---
929
+
930
+ ## Remote OPA Server Mode
931
+
932
+ Remote mode sends evaluation requests to an external OPA server via HTTP. This is useful for centralized policy management across multiple services.
933
+
934
+ ```yaml
935
+ policy:
936
+ engine: remote
937
+ url: http://opa:8181
938
+ fallback: deny
939
+ timeout: 3000
940
+ ```
941
+
942
+ ### How it works
943
+
944
+ 1. Visor sends POST requests to `${url}/v1/data/visor/<scope>`
945
+ 2. The request body is `{ "input": <policy-input-document> }`
946
+ 3. The response contains `{ "result": { "allowed": true/false, ... } }`
947
+
948
+ ### Setting up an OPA server
949
+
950
+ ```bash
951
+ # Run OPA as a server with your policies
952
+ opa run --server --addr :8181 ./policies/
953
+
954
+ # Or with Docker
955
+ docker run -p 8181:8181 \
956
+ -v $(pwd)/policies:/policies \
957
+ openpolicyagent/opa:latest \
958
+ run --server --addr :8181 /policies/
959
+ ```
960
+
961
+ ### When to use remote mode
962
+
963
+ - Centralized policy management across multiple repositories
964
+ - Policy bundles pulled from a registry
965
+ - Audit logging at the OPA server level
966
+ - Policies shared with other services (not just Visor)
967
+
968
+ ---
969
+
970
+ ## Fallback Behavior
971
+
972
+ The `fallback` setting controls what happens when policy evaluation fails or a policy denies an action:
973
+
974
+ | Setting | Behavior |
975
+ |---------|----------|
976
+ | `allow` (default) | On error/timeout, allow the action |
977
+ | `deny` | On error/timeout, deny the action |
978
+ | `warn` | Evaluate policies normally but never block execution. Denied actions are allowed to proceed, and a warning is logged instead. Use this for gradual policy rollout. |
979
+
980
+ ### Audit mode with `warn`
981
+
982
+ The `warn` fallback is designed for **gradual policy rollout**. When deploying new policies, set `fallback: warn` to observe what would be denied without actually blocking any checks. This lets you:
983
+
984
+ - Validate that your Rego policies match your intent before enforcing them
985
+ - Identify unexpected denials in production without disrupting workflows
986
+ - Gradually roll out policies: start with `warn`, review logs, then switch to `deny`
987
+
988
+ In warn mode:
989
+ - All policy evaluations run normally
990
+ - Denied decisions are overridden to allowed, with the original reason prefixed by `audit:`
991
+ - Warnings are emitted to the log: `[PolicyEngine] Audit: check '<id>' would be denied: <reason>`
992
+ - If policy evaluation fails (error/timeout), the action is still allowed and a warning is logged
993
+
994
+ ```yaml
995
+ # Example: observe policy decisions before enforcing
996
+ policy:
997
+ engine: local
998
+ rules: ./policies/
999
+ fallback: warn # log violations, don't block
1000
+ roles:
1001
+ admin:
1002
+ author_association: [OWNER]
1003
+ developer:
1004
+ author_association: [MEMBER, COLLABORATOR]
1005
+ ```
1006
+
1007
+ Evaluation can fail due to:
1008
+ - WASM compilation errors (invalid Rego syntax)
1009
+ - Timeout exceeded
1010
+ - Remote OPA server unreachable
1011
+ - Missing or invalid `.rego` files
1012
+ - Runtime evaluation errors in Rego
1013
+
1014
+ ### Without a license
1015
+
1016
+ If no valid license is found, the policy engine is **silently disabled**. All checks run as normal with no policy enforcement. No error is raised. This means:
1017
+
1018
+ - The OSS build works exactly as before
1019
+ - The EE build without a license works exactly as the OSS build
1020
+ - Expired licenses (past the 72h grace period) behave as if no license is present
1021
+
1022
+ ---
1023
+
1024
+ ## How It Works
1025
+
1026
+ ### Architecture
1027
+
1028
+ ```
1029
+ .visor.yaml (policy: block)
1030
+ |
1031
+ v
1032
+ src/policy/types.ts PolicyEngine interface (OSS)
1033
+ src/policy/default-engine.ts No-op implementation (OSS, always allows)
1034
+ |
1035
+ v (dynamic import, license-gated)
1036
+ src/enterprise/loader.ts Sole import boundary
1037
+ src/enterprise/policy/
1038
+ opa-policy-engine.ts Wraps WASM + HTTP evaluators
1039
+ opa-wasm-evaluator.ts @open-policy-agent/opa-wasm
1040
+ opa-http-evaluator.ts REST client for OPA server
1041
+ policy-input-builder.ts Builds OPA input documents
1042
+ ```
1043
+
1044
+ ### Execution flow
1045
+
1046
+ 1. **Engine startup**: If `config.policy.engine` is not `disabled`, Visor dynamically imports `src/enterprise/loader.ts`
1047
+ 2. **License check**: The loader validates the JWT license. If invalid or missing, returns `DefaultPolicyEngine` (no-op)
1048
+ 3. **OPA initialization**: For `local` mode, compiles `.rego` to WASM. For `remote` mode, validates the OPA server URL
1049
+ 4. **Check execution**: Before each check runs (after `if` conditions), the engine calls `policyEngine.evaluateCheckExecution()`
1050
+ 5. **Decision**: If denied, the check is skipped with `policy_denied` reason. If allowed, execution proceeds normally
1051
+
1052
+ ### Import boundary
1053
+
1054
+ The enterprise code is strictly isolated. OSS code never imports from `src/enterprise/` directly. The sole boundary is `src/enterprise/loader.ts`, loaded via dynamic `await import()`. This is enforced by an ESLint rule.
1055
+
1056
+ ---
1057
+
1058
+ ## Relationship to Author Permissions
1059
+
1060
+ Visor provides two mechanisms for permission-based workflow control:
1061
+
1062
+ | Feature | Author Permissions (OSS) | Policy Engine (EE) |
1063
+ |---------|--------------------------|-------------------|
1064
+ | **License** | None (OSS) | EE license required |
1065
+ | **Mechanism** | JavaScript expressions in `if`/`fail_if` | OPA Rego policies |
1066
+ | **Scope** | Per-step `if` conditions | Pre-execution gating, tool filtering, capability restriction |
1067
+ | **Enforcement** | Evaluated inline (can be bypassed by config changes) | Centralized, auditable, separable from config |
1068
+ | **Role system** | Uses `hasMinPermission()`, `isMember()`, etc. | Custom roles resolved from `policy.roles` config |
1069
+ | **Complexity** | Simple, inline | Full policy language (Rego) with testing tools |
1070
+
1071
+ ### When to use each
1072
+
1073
+ - **Author Permissions**: Simple permission checks embedded in step conditions. Good for small teams with straightforward rules.
1074
+ - **Policy Engine**: Centralized, auditable policy enforcement. Good for organizations that need compliance, separation of duties, or complex role hierarchies.
1075
+
1076
+ The two systems complement each other. Author permission functions remain available in `if`/`fail_if` expressions even when the policy engine is active. The policy engine evaluates first (before `if` conditions for check execution gating), providing an additional layer of control.
1077
+
1078
+ See [Author Permissions](./author-permissions.md) for the OSS permission functions.
1079
+
1080
+ ---
1081
+
1082
+ ## Troubleshooting
1083
+
1084
+ ### Policy engine not activating
1085
+
1086
+ **Symptom**: Checks run without policy enforcement even with `policy:` configured.
1087
+
1088
+ 1. **Check your license**: Ensure `VISOR_LICENSE` is set or `.visor-license` exists
1089
+ 2. **Verify the feature**: Your license must include the `policy` feature
1090
+ 3. **Check the engine setting**: Ensure `policy.engine` is `local` or `remote` (not `disabled`)
1091
+ 4. **Run with debug**: `visor --debug` shows policy initialization messages
1092
+
1093
+ ### OPA CLI not found
1094
+
1095
+ **Symptom**: `Error: opa command not found` at startup.
1096
+
1097
+ - Install the OPA CLI: see [Installation](#2-install-opa-cli-optional-for-local-compilation)
1098
+ - Or pre-compile your policies to `.wasm` to avoid needing the CLI at runtime
1099
+
1100
+ ### WASM compilation errors
1101
+
1102
+ **Symptom**: `opa build` fails at startup.
1103
+
1104
+ - Run `visor policy-check ./policies/` to validate syntax and WASM compatibility in one step
1105
+ - Check your Rego syntax: `opa check policies/`
1106
+ - Avoid WASM-unsafe patterns (see [WASM compilation safety](#writing-rego-policies))
1107
+ - Ensure the entrypoint package exists: your `.rego` files must declare `package visor.*` packages
1108
+
1109
+ ### All checks denied unexpectedly
1110
+
1111
+ **Symptom**: Every check shows `skipReason: policy_denied`.
1112
+
1113
+ - Verify your role definitions match the actor's GitHub association
1114
+ - Check `fallback: deny` vs `fallback: allow` — `deny` blocks on any evaluation error
1115
+ - Test your policy with `opa eval`:
1116
+ ```bash
1117
+ echo '{"actor":{"roles":["developer"]}}' | \
1118
+ opa eval -d policies/ -i /dev/stdin 'data.visor.check.execute.allowed'
1119
+ ```
1120
+
1121
+ ### Remote OPA server unreachable
1122
+
1123
+ **Symptom**: All checks are allowed/denied (based on fallback) when using remote mode.
1124
+
1125
+ - Verify the OPA server URL is correct and reachable
1126
+ - Check firewall rules and network connectivity
1127
+ - Increase `timeout` if the server is slow
1128
+ - Check OPA server logs for errors
1129
+
1130
+ ### Dumping the OPA input document
1131
+
1132
+ Use the `--dump-policy-input` flag to see the exact JSON input that Visor sends to OPA for a given check. This is invaluable for debugging Rego policies because you can feed the output directly into `opa eval`.
1133
+
1134
+ ```bash
1135
+ # Print the OPA input document for the "deploy-production" check
1136
+ visor --dump-policy-input deploy-production
1137
+
1138
+ # With a specific config file
1139
+ visor --dump-policy-input deploy-production --config ./my-visor.yaml
1140
+ ```
1141
+
1142
+ Example output:
1143
+
1144
+ ```json
1145
+ {
1146
+ "scope": "check.execute",
1147
+ "check": {
1148
+ "id": "deploy-production",
1149
+ "type": "command",
1150
+ "group": "deployment",
1151
+ "tags": ["deploy", "production"],
1152
+ "criticality": "external",
1153
+ "policy": {
1154
+ "require": "admin"
1155
+ }
1156
+ },
1157
+ "actor": {
1158
+ "login": "alice",
1159
+ "roles": ["developer"],
1160
+ "isLocalMode": true
1161
+ },
1162
+ "repository": {}
1163
+ }
1164
+ ```
1165
+
1166
+ You can pipe this directly into `opa eval` to test your policy:
1167
+
1168
+ ```bash
1169
+ visor --dump-policy-input deploy-production | \
1170
+ opa eval -d policies/ -i /dev/stdin 'data.visor.check.execute'
1171
+ ```
1172
+
1173
+ **Notes**:
1174
+ - This flag requires the **Enterprise Edition build** (the `src/enterprise/` modules must be present) but does **not** require a valid license key. It is a debugging tool that works without activation.
1175
+ - In OSS-only builds (where `src/enterprise/` is stripped), this flag is not available and will exit with an error.
1176
+ - Actor context is derived from environment variables (`VISOR_AUTHOR_LOGIN`, `GITHUB_ACTOR`, `VISOR_AUTHOR_ASSOCIATION`, `GITHUB_ACTIONS`).
1177
+ - Repository context is derived from GitHub environment variables (`GITHUB_REPOSITORY_OWNER`, `GITHUB_REPOSITORY`, `GITHUB_HEAD_REF`, `GITHUB_BASE_REF`, `GITHUB_EVENT_NAME`).
1178
+ - `repository.action` is **not populated** from any environment variable and will be absent.
1179
+ - `pullRequest.number` would require a `GITHUB_PR_NUMBER` environment variable, which is a custom env var that must be set manually (it is not provided by GitHub Actions by default and is not set by Visor). When running as a GitHub Action, PR context is enriched automatically from the PR info, but this field is not yet wired up to the policy input. It will be absent.
1180
+ - `pullRequest.labels`, `pullRequest.draft`, and `pullRequest.changedFiles` are **not currently populated** and will be absent.
1181
+ - When running locally (outside GitHub Actions), `actor.isLocalMode` is `true` and most repository fields will be empty.
1182
+
1183
+ ### Timeout errors
1184
+
1185
+ **Symptom**: Policy evaluations timing out.
1186
+
1187
+ - For local mode: this is rare (~1ms per evaluation). Check if `.rego` files are very complex
1188
+ - For remote mode: increase `timeout` or check network latency to the OPA server
1189
+ - Set `fallback: allow` if timeouts should not block execution
1190
+
1191
+ ---
1192
+
1193
+ ## Examples
1194
+
1195
+ ### Minimal setup
1196
+
1197
+ ```yaml
1198
+ # .visor.yaml
1199
+ version: "1.0"
1200
+
1201
+ policy:
1202
+ engine: local
1203
+ rules: ./policies/
1204
+ fallback: deny
1205
+ roles:
1206
+ admin:
1207
+ author_association: [OWNER]
1208
+ developer:
1209
+ author_association: [MEMBER, COLLABORATOR]
1210
+
1211
+ checks:
1212
+ security-scan:
1213
+ type: ai
1214
+ prompt: "Review for security issues"
1215
+ policy:
1216
+ require: developer
1217
+ ```
1218
+
1219
+ ```rego
1220
+ # policies/check_execute.rego
1221
+ package visor.check.execute
1222
+
1223
+ default allowed = false
1224
+
1225
+ # Explicit deny list (policy.deny in YAML) — deny takes precedence
1226
+ denied {
1227
+ some i, j
1228
+ input.check.policy.deny[i] == input.actor.roles[j]
1229
+ }
1230
+
1231
+ allowed {
1232
+ not denied
1233
+ input.actor.roles[_] == "admin"
1234
+ }
1235
+
1236
+ allowed {
1237
+ not denied
1238
+ required := input.check.policy.require
1239
+ is_string(required)
1240
+ input.actor.roles[_] == required
1241
+ }
1242
+
1243
+ # Local mode: allow checks without explicit policy requirements
1244
+ allowed {
1245
+ not denied
1246
+ input.actor.isLocalMode == true
1247
+ not input.check.policy
1248
+ }
1249
+
1250
+ reason = "role is in the deny list" { denied }
1251
+ reason = "insufficient role" { not denied; not allowed }
1252
+ ```
1253
+
1254
+ ### PR label and metadata policies
1255
+
1256
+ > **⚠️ Important**: The `pullRequest.labels`, `pullRequest.draft`, and `pullRequest.changedFiles` fields are **not currently populated** by the policy engine. The example below shows how these fields *would* be used if they were available, but attempting to use them now will result in empty/undefined values. See the [Input Document Reference](#input-document-reference) section for details on which fields are currently populated.
1257
+ >
1258
+ > This example is provided for future compatibility and to illustrate the intended design. If you need PR metadata in your policies today, you will need to extend Visor to fetch PR data and call `setActorContext()` with the enriched context.
1259
+
1260
+ ```rego
1261
+ # policies/check_execute.rego
1262
+ # FUTURE EXAMPLE - These fields are not yet populated
1263
+ package visor.check.execute
1264
+
1265
+ # Only allow deploy-production if PR has the "approved" label
1266
+ has_approved_label {
1267
+ input.pullRequest.labels[_] == "approved"
1268
+ }
1269
+
1270
+ allowed {
1271
+ not denied
1272
+ input.check.id == "deploy-production"
1273
+ has_approved_label
1274
+ input.actor.roles[_] == "admin"
1275
+ }
1276
+
1277
+ # Block all checks on draft PRs
1278
+ allowed = false {
1279
+ input.pullRequest.draft == true
1280
+ }
1281
+
1282
+ # Deny checks that change too many files (e.g., > 100)
1283
+ allowed = false {
1284
+ input.pullRequest.changedFiles > 100
1285
+ input.check.id == "full-review"
1286
+ }
1287
+
1288
+ reason = "PR must have 'approved' label for production deploy" {
1289
+ input.check.id == "deploy-production"
1290
+ not has_approved_label
1291
+ }
1292
+ reason = "checks are blocked on draft PRs" {
1293
+ input.pullRequest.draft == true
1294
+ }
1295
+ ```
1296
+
1297
+ ### Full example with all scopes
1298
+
1299
+ See [`examples/enterprise-policy/`](../examples/enterprise-policy/) for a complete working example with all three policy scopes, role definitions, and a ready-to-use `.visor.yaml`.
1300
+
1301
+ ### GitHub Actions integration
1302
+
1303
+ ```yaml
1304
+ # .github/workflows/visor.yml
1305
+ name: Visor
1306
+ on:
1307
+ pull_request: { types: [opened, synchronize] }
1308
+ permissions:
1309
+ contents: read
1310
+ pull-requests: write
1311
+ checks: write
1312
+ jobs:
1313
+ visor:
1314
+ runs-on: ubuntu-latest
1315
+ steps:
1316
+ - uses: actions/checkout@v4
1317
+ - uses: probelabs/visor@v1
1318
+ env:
1319
+ VISOR_LICENSE: ${{ secrets.VISOR_LICENSE }}
1320
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
1321
+ ```
1322
+
1323
+ ---
1324
+
1325
+ **Questions? Need a license?** Contact **hello@probelabs.com**