@probelabs/visor 0.1.130 → 0.1.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/defaults/visor.yaml +5 -2
- package/dist/ai-review-service.d.ts +2 -0
- package/dist/ai-review-service.d.ts.map +1 -1
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/config/cli-handler.d.ts +5 -0
- package/dist/config/cli-handler.d.ts.map +1 -0
- package/dist/config/config-reloader.d.ts +24 -0
- package/dist/config/config-reloader.d.ts.map +1 -0
- package/dist/config/config-snapshot-store.d.ts +21 -0
- package/dist/config/config-snapshot-store.d.ts.map +1 -0
- package/dist/config/config-watcher.d.ts +19 -0
- package/dist/config/config-watcher.d.ts.map +1 -0
- package/dist/config/types.d.ts +16 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/defaults/visor.yaml +5 -2
- package/dist/docs/ai-configuration.md +139 -0
- package/dist/docs/ai-custom-tools.md +30 -0
- package/dist/docs/capacity-planning.md +359 -0
- package/dist/docs/commands.md +35 -0
- package/dist/docs/database-operations.md +487 -0
- package/dist/docs/index.md +6 -1
- package/dist/docs/licensing.md +372 -0
- package/dist/docs/production-deployment.md +583 -0
- package/dist/examples/ai-with-bash.yaml +17 -0
- package/dist/generated/config-schema.d.ts +4 -0
- package/dist/generated/config-schema.d.ts.map +1 -1
- package/dist/index.js +9945 -10907
- package/dist/liquid-extensions.d.ts +7 -0
- package/dist/liquid-extensions.d.ts.map +1 -1
- package/dist/output/traces/{run-2026-02-11T16-20-59-999Z.ndjson → run-2026-02-15T19-14-20-379Z.ndjson} +84 -84
- package/dist/{traces/run-2026-02-11T16-21-47-711Z.ndjson → output/traces/run-2026-02-15T19-15-09-410Z.ndjson} +1019 -1019
- package/dist/providers/ai-check-provider.d.ts +5 -0
- package/dist/providers/ai-check-provider.d.ts.map +1 -1
- package/dist/providers/command-check-provider.d.ts.map +1 -1
- package/dist/providers/workflow-check-provider.d.ts.map +1 -1
- package/dist/scheduler/schedule-tool.d.ts.map +1 -1
- package/dist/sdk/{check-provider-registry-PANIXYRB.mjs → check-provider-registry-AAPPJ4CP.mjs} +7 -7
- package/dist/sdk/{check-provider-registry-M3Y6JMTW.mjs → check-provider-registry-S7BMQ2FC.mjs} +7 -7
- package/dist/sdk/check-provider-registry-ZOLEYDKM.mjs +28 -0
- package/dist/sdk/{chunk-VMLORODQ.mjs → chunk-2GCSK3PD.mjs} +4 -4
- package/dist/sdk/{chunk-EUUAQBTW.mjs → chunk-6ZZ4DPAA.mjs} +240 -48
- package/dist/sdk/chunk-6ZZ4DPAA.mjs.map +1 -0
- package/dist/sdk/{chunk-HOKQOO3G.mjs → chunk-EBTD2D4L.mjs} +2 -2
- package/dist/sdk/chunk-LDFUW34H.mjs +39912 -0
- package/dist/sdk/chunk-LDFUW34H.mjs.map +1 -0
- package/dist/sdk/{chunk-UCNT3PDT.mjs → chunk-LQ5B4T6L.mjs} +5 -1
- package/dist/sdk/chunk-LQ5B4T6L.mjs.map +1 -0
- package/dist/sdk/{chunk-S6CD7GFM.mjs → chunk-MQ57AB4U.mjs} +211 -35
- package/dist/sdk/chunk-MQ57AB4U.mjs.map +1 -0
- package/dist/sdk/chunk-N4I6ZDCJ.mjs +436 -0
- package/dist/sdk/chunk-N4I6ZDCJ.mjs.map +1 -0
- package/dist/sdk/chunk-OMFPM576.mjs +739 -0
- package/dist/sdk/chunk-OMFPM576.mjs.map +1 -0
- package/dist/sdk/chunk-RI77TA6V.mjs +436 -0
- package/dist/sdk/chunk-RI77TA6V.mjs.map +1 -0
- package/dist/sdk/chunk-VO4N6TEL.mjs +1502 -0
- package/dist/sdk/chunk-VO4N6TEL.mjs.map +1 -0
- package/dist/sdk/{chunk-V2IV3ILA.mjs → chunk-XJQKTK6V.mjs} +31 -5
- package/dist/sdk/chunk-XJQKTK6V.mjs.map +1 -0
- package/dist/sdk/{config-OGOS4ZU4.mjs → config-4EG7IQIU.mjs} +2 -2
- package/dist/sdk/{failure-condition-evaluator-HC3M5377.mjs → failure-condition-evaluator-GLHZZF47.mjs} +3 -3
- package/dist/sdk/failure-condition-evaluator-KN55WXRO.mjs +17 -0
- package/dist/sdk/{github-frontend-E2KJSC3Y.mjs → github-frontend-F4TE2JY7.mjs} +3 -3
- package/dist/sdk/github-frontend-HCOKL53D.mjs +1356 -0
- package/dist/sdk/github-frontend-HCOKL53D.mjs.map +1 -0
- package/dist/sdk/{host-EE6EJ2FM.mjs → host-SAT6RHDX.mjs} +2 -2
- package/dist/sdk/host-VA3ET7N6.mjs +63 -0
- package/dist/sdk/host-VA3ET7N6.mjs.map +1 -0
- package/dist/sdk/{liquid-extensions-E4EUOCES.mjs → liquid-extensions-YDIIH33Q.mjs} +2 -2
- package/dist/sdk/{routing-OZQWAGAI.mjs → routing-KFYQGOYU.mjs} +5 -5
- package/dist/sdk/routing-OXQKETSA.mjs +25 -0
- package/dist/sdk/{schedule-tool-handler-IEB2VS7O.mjs → schedule-tool-handler-G353DHS6.mjs} +7 -7
- package/dist/sdk/{schedule-tool-handler-B7TMSG6A.mjs → schedule-tool-handler-OQF57URO.mjs} +7 -7
- package/dist/sdk/schedule-tool-handler-PJVKWSYX.mjs +38 -0
- package/dist/sdk/schedule-tool-handler-PJVKWSYX.mjs.map +1 -0
- package/dist/sdk/sdk.d.mts +15 -0
- package/dist/sdk/sdk.d.ts +15 -0
- package/dist/sdk/sdk.js +621 -183
- package/dist/sdk/sdk.js.map +1 -1
- package/dist/sdk/sdk.mjs +6 -6
- package/dist/sdk/{trace-helpers-PP3YHTAM.mjs → trace-helpers-LOPBHYYX.mjs} +4 -2
- package/dist/sdk/trace-helpers-LOPBHYYX.mjs.map +1 -0
- package/dist/sdk/trace-helpers-R2ETIEC2.mjs +25 -0
- package/dist/sdk/trace-helpers-R2ETIEC2.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-2ET3SFZH.mjs → workflow-check-provider-57KAR4Y4.mjs} +7 -7
- package/dist/sdk/workflow-check-provider-57KAR4Y4.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-HB4XTD4Z.mjs → workflow-check-provider-LRWD52WN.mjs} +7 -7
- package/dist/sdk/workflow-check-provider-LRWD52WN.mjs.map +1 -0
- package/dist/sdk/workflow-check-provider-N2DRFQDB.mjs +28 -0
- package/dist/sdk/workflow-check-provider-N2DRFQDB.mjs.map +1 -0
- package/dist/slack/socket-runner.d.ts.map +1 -1
- package/dist/state-machine/context/build-engine-context.d.ts.map +1 -1
- package/dist/state-machine/runner.d.ts.map +1 -1
- package/dist/state-machine/states/completed.d.ts.map +1 -1
- package/dist/telemetry/trace-helpers.d.ts +5 -0
- package/dist/telemetry/trace-helpers.d.ts.map +1 -1
- package/dist/test-runner/evaluators.d.ts.map +1 -1
- package/dist/test-runner/index.d.ts +7 -0
- package/dist/test-runner/index.d.ts.map +1 -1
- package/dist/test-runner/validator.d.ts.map +1 -1
- package/dist/traces/{run-2026-02-11T16-20-59-999Z.ndjson → run-2026-02-15T19-14-20-379Z.ndjson} +84 -84
- package/dist/{output/traces/run-2026-02-11T16-21-47-711Z.ndjson → traces/run-2026-02-15T19-15-09-410Z.ndjson} +1019 -1019
- package/dist/tui/chat-runner.d.ts.map +1 -1
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/config.d.ts +15 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/engine.d.ts +2 -0
- package/dist/types/engine.d.ts.map +1 -1
- package/package.json +3 -3
- package/defaults/.visor.yaml +0 -420
- package/dist/sdk/chunk-EUUAQBTW.mjs.map +0 -1
- package/dist/sdk/chunk-S6CD7GFM.mjs.map +0 -1
- package/dist/sdk/chunk-UCNT3PDT.mjs.map +0 -1
- package/dist/sdk/chunk-V2IV3ILA.mjs.map +0 -1
- package/dist/sdk/chunk-YJRBN3XS.mjs +0 -217
- package/dist/sdk/chunk-YJRBN3XS.mjs.map +0 -1
- /package/dist/sdk/{check-provider-registry-M3Y6JMTW.mjs.map → check-provider-registry-AAPPJ4CP.mjs.map} +0 -0
- /package/dist/sdk/{check-provider-registry-PANIXYRB.mjs.map → check-provider-registry-S7BMQ2FC.mjs.map} +0 -0
- /package/dist/sdk/{config-OGOS4ZU4.mjs.map → check-provider-registry-ZOLEYDKM.mjs.map} +0 -0
- /package/dist/sdk/{chunk-VMLORODQ.mjs.map → chunk-2GCSK3PD.mjs.map} +0 -0
- /package/dist/sdk/{chunk-HOKQOO3G.mjs.map → chunk-EBTD2D4L.mjs.map} +0 -0
- /package/dist/sdk/{failure-condition-evaluator-HC3M5377.mjs.map → config-4EG7IQIU.mjs.map} +0 -0
- /package/dist/sdk/{liquid-extensions-E4EUOCES.mjs.map → failure-condition-evaluator-GLHZZF47.mjs.map} +0 -0
- /package/dist/sdk/{routing-OZQWAGAI.mjs.map → failure-condition-evaluator-KN55WXRO.mjs.map} +0 -0
- /package/dist/sdk/{github-frontend-E2KJSC3Y.mjs.map → github-frontend-F4TE2JY7.mjs.map} +0 -0
- /package/dist/sdk/{host-EE6EJ2FM.mjs.map → host-SAT6RHDX.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-B7TMSG6A.mjs.map → liquid-extensions-YDIIH33Q.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-IEB2VS7O.mjs.map → routing-KFYQGOYU.mjs.map} +0 -0
- /package/dist/sdk/{trace-helpers-PP3YHTAM.mjs.map → routing-OXQKETSA.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-2ET3SFZH.mjs.map → schedule-tool-handler-G353DHS6.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-HB4XTD4Z.mjs.map → schedule-tool-handler-OQF57URO.mjs.map} +0 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# Production Deployment Guide
|
|
2
|
+
|
|
3
|
+
This guide covers deploying Visor as a production service across Docker, Kubernetes, and traditional server environments.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Architecture Overview](#architecture-overview)
|
|
10
|
+
- [Docker Deployment](#docker-deployment)
|
|
11
|
+
- [Docker Compose](#docker-compose)
|
|
12
|
+
- [Kubernetes Deployment](#kubernetes-deployment)
|
|
13
|
+
- [Environment Variables](#environment-variables)
|
|
14
|
+
- [Health Checks and Readiness](#health-checks-and-readiness)
|
|
15
|
+
- [Security Hardening](#security-hardening)
|
|
16
|
+
- [Multi-Instance / High Availability](#multi-instance--high-availability)
|
|
17
|
+
- [Logging and Monitoring](#logging-and-monitoring)
|
|
18
|
+
- [Backup and Recovery](#backup-and-recovery)
|
|
19
|
+
- [Upgrading](#upgrading)
|
|
20
|
+
- [Troubleshooting](#troubleshooting)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Architecture Overview
|
|
25
|
+
|
|
26
|
+
A production Visor deployment typically consists of:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
+------------------+
|
|
30
|
+
| GitHub / Slack |
|
|
31
|
+
+--------+---------+
|
|
32
|
+
|
|
|
33
|
+
+----------+----------+
|
|
34
|
+
| Load Balancer / |
|
|
35
|
+
| Ingress Controller|
|
|
36
|
+
+----------+----------+
|
|
37
|
+
|
|
|
38
|
+
+----------------+----------------+
|
|
39
|
+
| | |
|
|
40
|
+
+-----v-----+ +-----v-----+ +-----v-----+
|
|
41
|
+
| Visor #1 | | Visor #2 | | Visor #3 |
|
|
42
|
+
| (--slack) | | (--slack) | | (--slack) |
|
|
43
|
+
+-----+------+ +-----+------+ +-----+------+
|
|
44
|
+
| | |
|
|
45
|
+
+----------------+----------------+
|
|
46
|
+
|
|
|
47
|
+
+---------v---------+
|
|
48
|
+
| PostgreSQL / |
|
|
49
|
+
| MySQL / MSSQL |
|
|
50
|
+
+-------------------+
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Components:**
|
|
54
|
+
- **Visor instances** run in `--slack` mode (long-running) or as CI jobs (ephemeral)
|
|
55
|
+
- **Database** stores scheduler state and config snapshots (PostgreSQL recommended)
|
|
56
|
+
- **External services**: GitHub API, Slack API, AI providers (Gemini, Claude, OpenAI)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Docker Deployment
|
|
61
|
+
|
|
62
|
+
### Dockerfile
|
|
63
|
+
|
|
64
|
+
```dockerfile
|
|
65
|
+
FROM node:20-alpine
|
|
66
|
+
|
|
67
|
+
WORKDIR /app
|
|
68
|
+
|
|
69
|
+
# Install Visor
|
|
70
|
+
RUN npm install -g @probelabs/visor
|
|
71
|
+
|
|
72
|
+
# For EE features with OPA policies:
|
|
73
|
+
# RUN npm install -g @probelabs/visor@ee
|
|
74
|
+
# RUN apk add --no-cache curl && \
|
|
75
|
+
# curl -L -o /usr/local/bin/opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static && \
|
|
76
|
+
# chmod +x /usr/local/bin/opa
|
|
77
|
+
|
|
78
|
+
# Copy configuration
|
|
79
|
+
COPY .visor.yaml /app/.visor.yaml
|
|
80
|
+
|
|
81
|
+
# Optional: copy policy files
|
|
82
|
+
# COPY policies/ /app/policies/
|
|
83
|
+
|
|
84
|
+
# Non-root user
|
|
85
|
+
RUN addgroup -g 1001 visor && \
|
|
86
|
+
adduser -D -u 1001 -G visor visor && \
|
|
87
|
+
chown -R visor:visor /app
|
|
88
|
+
USER visor
|
|
89
|
+
|
|
90
|
+
ENTRYPOINT ["visor"]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Running
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Build
|
|
97
|
+
docker build -t visor:latest .
|
|
98
|
+
|
|
99
|
+
# Run checks (ephemeral)
|
|
100
|
+
docker run --rm \
|
|
101
|
+
-e GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
|
102
|
+
-e GEMINI_API_KEY="${GEMINI_API_KEY}" \
|
|
103
|
+
-v "$(pwd):/workspace" \
|
|
104
|
+
visor:latest --config /workspace/.visor.yaml --check all
|
|
105
|
+
|
|
106
|
+
# Run Slack mode (long-running)
|
|
107
|
+
docker run -d \
|
|
108
|
+
--name visor \
|
|
109
|
+
--restart unless-stopped \
|
|
110
|
+
-e SLACK_APP_TOKEN="${SLACK_APP_TOKEN}" \
|
|
111
|
+
-e GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
|
112
|
+
-e GEMINI_API_KEY="${GEMINI_API_KEY}" \
|
|
113
|
+
visor:latest --slack --config /app/.visor.yaml --watch
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Docker Compose
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
version: "3.8"
|
|
122
|
+
|
|
123
|
+
services:
|
|
124
|
+
visor:
|
|
125
|
+
build: .
|
|
126
|
+
restart: unless-stopped
|
|
127
|
+
command: ["--slack", "--config", "/app/.visor.yaml", "--watch"]
|
|
128
|
+
environment:
|
|
129
|
+
SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}"
|
|
130
|
+
GITHUB_TOKEN: "${GITHUB_TOKEN}"
|
|
131
|
+
GEMINI_API_KEY: "${GEMINI_API_KEY}"
|
|
132
|
+
# Database (EE)
|
|
133
|
+
VISOR_DB_PASSWORD: "${VISOR_DB_PASSWORD}"
|
|
134
|
+
# Telemetry
|
|
135
|
+
VISOR_TELEMETRY_ENABLED: "true"
|
|
136
|
+
VISOR_TELEMETRY_SINK: "otlp"
|
|
137
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://jaeger:4318/v1/traces"
|
|
138
|
+
volumes:
|
|
139
|
+
- visor-data:/app/.visor
|
|
140
|
+
- ./config/.visor.yaml:/app/.visor.yaml:ro
|
|
141
|
+
depends_on:
|
|
142
|
+
db:
|
|
143
|
+
condition: service_healthy
|
|
144
|
+
|
|
145
|
+
db:
|
|
146
|
+
image: postgres:16-alpine
|
|
147
|
+
restart: unless-stopped
|
|
148
|
+
environment:
|
|
149
|
+
POSTGRES_DB: visor
|
|
150
|
+
POSTGRES_USER: visor
|
|
151
|
+
POSTGRES_PASSWORD: "${VISOR_DB_PASSWORD}"
|
|
152
|
+
volumes:
|
|
153
|
+
- pgdata:/var/lib/postgresql/data
|
|
154
|
+
healthcheck:
|
|
155
|
+
test: ["CMD-SHELL", "pg_isready -U visor"]
|
|
156
|
+
interval: 10s
|
|
157
|
+
timeout: 5s
|
|
158
|
+
retries: 5
|
|
159
|
+
|
|
160
|
+
jaeger:
|
|
161
|
+
image: jaegertracing/all-in-one:latest
|
|
162
|
+
ports:
|
|
163
|
+
- "16686:16686" # UI
|
|
164
|
+
- "4318:4318" # OTLP HTTP
|
|
165
|
+
|
|
166
|
+
volumes:
|
|
167
|
+
visor-data:
|
|
168
|
+
pgdata:
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Start with:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
docker compose up -d
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Kubernetes Deployment
|
|
180
|
+
|
|
181
|
+
### Namespace and Secret
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
apiVersion: v1
|
|
185
|
+
kind: Namespace
|
|
186
|
+
metadata:
|
|
187
|
+
name: visor
|
|
188
|
+
---
|
|
189
|
+
apiVersion: v1
|
|
190
|
+
kind: Secret
|
|
191
|
+
metadata:
|
|
192
|
+
name: visor-secrets
|
|
193
|
+
namespace: visor
|
|
194
|
+
type: Opaque
|
|
195
|
+
stringData:
|
|
196
|
+
SLACK_APP_TOKEN: "xapp-..."
|
|
197
|
+
GITHUB_TOKEN: "ghp_..."
|
|
198
|
+
GEMINI_API_KEY: "AIza..."
|
|
199
|
+
VISOR_DB_PASSWORD: "changeme"
|
|
200
|
+
# EE license (if applicable)
|
|
201
|
+
# VISOR_LICENSE: "eyJ..."
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### ConfigMap
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
apiVersion: v1
|
|
208
|
+
kind: ConfigMap
|
|
209
|
+
metadata:
|
|
210
|
+
name: visor-config
|
|
211
|
+
namespace: visor
|
|
212
|
+
data:
|
|
213
|
+
.visor.yaml: |
|
|
214
|
+
version: "1.0"
|
|
215
|
+
checks:
|
|
216
|
+
security:
|
|
217
|
+
type: ai
|
|
218
|
+
prompt: "Review for security issues"
|
|
219
|
+
scheduler:
|
|
220
|
+
enabled: true
|
|
221
|
+
storage:
|
|
222
|
+
driver: postgresql
|
|
223
|
+
connection:
|
|
224
|
+
host: postgres.visor.svc.cluster.local
|
|
225
|
+
port: 5432
|
|
226
|
+
database: visor
|
|
227
|
+
user: visor
|
|
228
|
+
password: ${VISOR_DB_PASSWORD}
|
|
229
|
+
ssl: false
|
|
230
|
+
pool:
|
|
231
|
+
min: 2
|
|
232
|
+
max: 10
|
|
233
|
+
ha:
|
|
234
|
+
enabled: true
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Deployment
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
apiVersion: apps/v1
|
|
241
|
+
kind: Deployment
|
|
242
|
+
metadata:
|
|
243
|
+
name: visor
|
|
244
|
+
namespace: visor
|
|
245
|
+
spec:
|
|
246
|
+
replicas: 2
|
|
247
|
+
selector:
|
|
248
|
+
matchLabels:
|
|
249
|
+
app: visor
|
|
250
|
+
template:
|
|
251
|
+
metadata:
|
|
252
|
+
labels:
|
|
253
|
+
app: visor
|
|
254
|
+
spec:
|
|
255
|
+
containers:
|
|
256
|
+
- name: visor
|
|
257
|
+
image: your-registry/visor:latest
|
|
258
|
+
args: ["--slack", "--config", "/config/.visor.yaml", "--watch"]
|
|
259
|
+
envFrom:
|
|
260
|
+
- secretRef:
|
|
261
|
+
name: visor-secrets
|
|
262
|
+
volumeMounts:
|
|
263
|
+
- name: config
|
|
264
|
+
mountPath: /config
|
|
265
|
+
readOnly: true
|
|
266
|
+
- name: data
|
|
267
|
+
mountPath: /app/.visor
|
|
268
|
+
resources:
|
|
269
|
+
requests:
|
|
270
|
+
cpu: 250m
|
|
271
|
+
memory: 256Mi
|
|
272
|
+
limits:
|
|
273
|
+
cpu: "1"
|
|
274
|
+
memory: 512Mi
|
|
275
|
+
livenessProbe:
|
|
276
|
+
exec:
|
|
277
|
+
command: ["sh", "-c", "pgrep -f visor || exit 1"]
|
|
278
|
+
initialDelaySeconds: 10
|
|
279
|
+
periodSeconds: 30
|
|
280
|
+
readinessProbe:
|
|
281
|
+
exec:
|
|
282
|
+
command: ["sh", "-c", "pgrep -f visor || exit 1"]
|
|
283
|
+
initialDelaySeconds: 5
|
|
284
|
+
periodSeconds: 10
|
|
285
|
+
volumes:
|
|
286
|
+
- name: config
|
|
287
|
+
configMap:
|
|
288
|
+
name: visor-config
|
|
289
|
+
- name: data
|
|
290
|
+
emptyDir: {}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### PostgreSQL (StatefulSet)
|
|
294
|
+
|
|
295
|
+
For production PostgreSQL in Kubernetes, consider using an operator like [CloudNativePG](https://cloudnative-pg.io/) or a managed service (RDS, Cloud SQL, Azure Database). A minimal StatefulSet for reference:
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
apiVersion: apps/v1
|
|
299
|
+
kind: StatefulSet
|
|
300
|
+
metadata:
|
|
301
|
+
name: postgres
|
|
302
|
+
namespace: visor
|
|
303
|
+
spec:
|
|
304
|
+
serviceName: postgres
|
|
305
|
+
replicas: 1
|
|
306
|
+
selector:
|
|
307
|
+
matchLabels:
|
|
308
|
+
app: postgres
|
|
309
|
+
template:
|
|
310
|
+
metadata:
|
|
311
|
+
labels:
|
|
312
|
+
app: postgres
|
|
313
|
+
spec:
|
|
314
|
+
containers:
|
|
315
|
+
- name: postgres
|
|
316
|
+
image: postgres:16-alpine
|
|
317
|
+
env:
|
|
318
|
+
- name: POSTGRES_DB
|
|
319
|
+
value: visor
|
|
320
|
+
- name: POSTGRES_USER
|
|
321
|
+
value: visor
|
|
322
|
+
- name: POSTGRES_PASSWORD
|
|
323
|
+
valueFrom:
|
|
324
|
+
secretKeyRef:
|
|
325
|
+
name: visor-secrets
|
|
326
|
+
key: VISOR_DB_PASSWORD
|
|
327
|
+
ports:
|
|
328
|
+
- containerPort: 5432
|
|
329
|
+
volumeMounts:
|
|
330
|
+
- name: pgdata
|
|
331
|
+
mountPath: /var/lib/postgresql/data
|
|
332
|
+
volumeClaimTemplates:
|
|
333
|
+
- metadata:
|
|
334
|
+
name: pgdata
|
|
335
|
+
spec:
|
|
336
|
+
accessModes: ["ReadWriteOnce"]
|
|
337
|
+
resources:
|
|
338
|
+
requests:
|
|
339
|
+
storage: 5Gi
|
|
340
|
+
---
|
|
341
|
+
apiVersion: v1
|
|
342
|
+
kind: Service
|
|
343
|
+
metadata:
|
|
344
|
+
name: postgres
|
|
345
|
+
namespace: visor
|
|
346
|
+
spec:
|
|
347
|
+
selector:
|
|
348
|
+
app: postgres
|
|
349
|
+
ports:
|
|
350
|
+
- port: 5432
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Triggering Config Reload in Kubernetes
|
|
354
|
+
|
|
355
|
+
When the ConfigMap changes, send `SIGUSR2` to the Visor process:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
kubectl exec -n visor deploy/visor -- kill -USR2 1
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Or use a sidecar like [reloader](https://github.com/stakater/Reloader) to auto-restart on ConfigMap changes.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Environment Variables
|
|
366
|
+
|
|
367
|
+
| Variable | Required | Description |
|
|
368
|
+
|----------|----------|-------------|
|
|
369
|
+
| `GITHUB_TOKEN` | Yes (GitHub mode) | GitHub personal access token or app token |
|
|
370
|
+
| `SLACK_APP_TOKEN` | Yes (Slack mode) | Slack app-level token (`xapp-...`) |
|
|
371
|
+
| `GEMINI_API_KEY` | Provider-specific | Google Gemini API key |
|
|
372
|
+
| `ANTHROPIC_API_KEY` | Provider-specific | Anthropic Claude API key |
|
|
373
|
+
| `OPENAI_API_KEY` | Provider-specific | OpenAI API key |
|
|
374
|
+
| `VISOR_LICENSE` | EE only | Enterprise license JWT |
|
|
375
|
+
| `VISOR_LICENSE_FILE` | EE only | Path to license file (alternative to `VISOR_LICENSE`) |
|
|
376
|
+
| `VISOR_DB_PASSWORD` | EE + SQL | Database password (used in config via `${VISOR_DB_PASSWORD}`) |
|
|
377
|
+
| `VISOR_TELEMETRY_ENABLED` | No | Enable OpenTelemetry (`true`/`false`) |
|
|
378
|
+
| `VISOR_TELEMETRY_SINK` | No | Telemetry sink: `otlp`, `file`, `console` |
|
|
379
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | No | OTLP endpoint URL |
|
|
380
|
+
| `VISOR_WORKSPACE_PATH` | No | Override workspace base path |
|
|
381
|
+
| `VISOR_DEBUG` | No | Enable debug logging (`true`/`false`) |
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Security Hardening
|
|
386
|
+
|
|
387
|
+
### Secrets Management
|
|
388
|
+
|
|
389
|
+
- **Never** commit secrets to `.visor.yaml`. Use environment variable references (`${VAR_NAME}`).
|
|
390
|
+
- Use Kubernetes Secrets, AWS Secrets Manager, or HashiCorp Vault for secret injection.
|
|
391
|
+
- Rotate API keys and tokens periodically.
|
|
392
|
+
|
|
393
|
+
### Network
|
|
394
|
+
|
|
395
|
+
- Run Visor in a private subnet with outbound-only internet (for API calls to GitHub, Slack, AI providers).
|
|
396
|
+
- Restrict database access to Visor instances only (security groups / network policies).
|
|
397
|
+
- Use TLS for all database connections in production (`ssl: true`).
|
|
398
|
+
|
|
399
|
+
### Container
|
|
400
|
+
|
|
401
|
+
- Run as non-root user (UID 1001 in the Dockerfile above).
|
|
402
|
+
- Use read-only root filesystem where possible.
|
|
403
|
+
- Set resource limits to prevent runaway processes.
|
|
404
|
+
- Pin image tags to specific versions (not `latest`).
|
|
405
|
+
|
|
406
|
+
### EE Policy Engine
|
|
407
|
+
|
|
408
|
+
- Use `fallback: deny` to block unrecognized actions by default.
|
|
409
|
+
- Define explicit roles and scope policies to each team.
|
|
410
|
+
- Audit policy decisions with `fallback: warn` before switching to `deny`.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Multi-Instance / High Availability
|
|
415
|
+
|
|
416
|
+
For multi-instance deployments (e.g., Kubernetes replicas > 1):
|
|
417
|
+
|
|
418
|
+
1. **Use a SQL database** (PostgreSQL recommended) instead of SQLite.
|
|
419
|
+
2. **Enable HA mode** in scheduler config:
|
|
420
|
+
|
|
421
|
+
```yaml
|
|
422
|
+
scheduler:
|
|
423
|
+
storage:
|
|
424
|
+
driver: postgresql
|
|
425
|
+
connection:
|
|
426
|
+
host: postgres.visor.svc.cluster.local
|
|
427
|
+
database: visor
|
|
428
|
+
user: visor
|
|
429
|
+
password: ${VISOR_DB_PASSWORD}
|
|
430
|
+
ha:
|
|
431
|
+
enabled: true
|
|
432
|
+
lock_ttl: 60
|
|
433
|
+
heartbeat_interval: 15
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
3. **Each instance must have a unique `node_id`**. By default, Visor uses `hostname-pid` which is unique in containers.
|
|
437
|
+
4. **Slack mode**: Multiple replicas can connect to the same Slack app. Socket Mode distributes events across connected instances.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Logging and Monitoring
|
|
442
|
+
|
|
443
|
+
### Structured Logging
|
|
444
|
+
|
|
445
|
+
Visor logs to stderr in structured format. Redirect to your log aggregator:
|
|
446
|
+
|
|
447
|
+
```yaml
|
|
448
|
+
# Kubernetes: logs are collected automatically by fluentd/fluent-bit
|
|
449
|
+
# Docker: use --log-driver
|
|
450
|
+
docker run --log-driver=json-file --log-opt max-size=100m ...
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### OpenTelemetry
|
|
454
|
+
|
|
455
|
+
Enable tracing for full execution visibility:
|
|
456
|
+
|
|
457
|
+
```yaml
|
|
458
|
+
# .visor.yaml
|
|
459
|
+
telemetry:
|
|
460
|
+
enabled: true
|
|
461
|
+
sink: otlp
|
|
462
|
+
tracing:
|
|
463
|
+
auto_instrumentations: true
|
|
464
|
+
trace_report:
|
|
465
|
+
enabled: true
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
# Environment
|
|
470
|
+
VISOR_TELEMETRY_ENABLED=true
|
|
471
|
+
VISOR_TELEMETRY_SINK=otlp
|
|
472
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4318/v1/traces
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
See [Telemetry Setup](./telemetry-setup.md) and [Dashboards](./dashboards/README.md) for Grafana dashboard templates.
|
|
476
|
+
|
|
477
|
+
### Key Metrics to Monitor
|
|
478
|
+
|
|
479
|
+
- **Check execution duration**: Track via OTel spans (`visor.check.*`)
|
|
480
|
+
- **Check success/failure rates**: Available in execution statistics
|
|
481
|
+
- **Scheduler lock contention**: Watch for `Timeout acquiring a connection` errors
|
|
482
|
+
- **AI provider latency and errors**: Track via provider-level spans
|
|
483
|
+
- **Memory and CPU usage**: Standard container metrics
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Backup and Recovery
|
|
488
|
+
|
|
489
|
+
### SQLite (single-node)
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
# Backup
|
|
493
|
+
cp .visor/schedules.db .visor/schedules.db.bak
|
|
494
|
+
cp .visor/config.db .visor/config.db.bak
|
|
495
|
+
|
|
496
|
+
# Restore
|
|
497
|
+
cp .visor/schedules.db.bak .visor/schedules.db
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### PostgreSQL
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
# Backup
|
|
504
|
+
pg_dump -h db.example.com -U visor visor > visor-backup.sql
|
|
505
|
+
|
|
506
|
+
# Restore
|
|
507
|
+
psql -h db.example.com -U visor visor < visor-backup.sql
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
For automated backups, see [Database Operations](./database-operations.md).
|
|
511
|
+
|
|
512
|
+
### Config Snapshots
|
|
513
|
+
|
|
514
|
+
Visor automatically snapshots resolved configuration at startup and on reload. Use the `visor config` command to list, view, and restore snapshots:
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
visor config snapshots # List snapshots
|
|
518
|
+
visor config show 1 # View snapshot YAML
|
|
519
|
+
visor config restore 1 --output restored.yaml
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Upgrading
|
|
525
|
+
|
|
526
|
+
### Rolling Update (Kubernetes)
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
# Update image
|
|
530
|
+
kubectl set image -n visor deployment/visor visor=your-registry/visor:v1.2.3
|
|
531
|
+
|
|
532
|
+
# Monitor rollout
|
|
533
|
+
kubectl rollout status -n visor deployment/visor
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Docker Compose
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
docker compose pull
|
|
540
|
+
docker compose up -d
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Pre-upgrade Checklist
|
|
544
|
+
|
|
545
|
+
1. **Read release notes** for breaking changes.
|
|
546
|
+
2. **Back up the database** before upgrading.
|
|
547
|
+
3. **Validate config** with the new version: `visor validate --config .visor.yaml`
|
|
548
|
+
4. **Test in staging** before production rollout.
|
|
549
|
+
5. For EE: Verify your license is compatible with the new version.
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Troubleshooting
|
|
554
|
+
|
|
555
|
+
### Container Exits Immediately
|
|
556
|
+
|
|
557
|
+
- Check logs: `docker logs visor` or `kubectl logs -n visor deploy/visor`
|
|
558
|
+
- Verify `--config` path is correct and mounted.
|
|
559
|
+
- Ensure required environment variables are set.
|
|
560
|
+
|
|
561
|
+
### Cannot Connect to Database
|
|
562
|
+
|
|
563
|
+
- Verify network connectivity: `nc -zv db.example.com 5432`
|
|
564
|
+
- Check credentials and database name.
|
|
565
|
+
- For Kubernetes: Ensure the database Service is in the same namespace or use FQDN.
|
|
566
|
+
|
|
567
|
+
### Slack Mode Not Receiving Events
|
|
568
|
+
|
|
569
|
+
- Verify `SLACK_APP_TOKEN` is a valid app-level token (`xapp-...`).
|
|
570
|
+
- Check that Socket Mode is enabled in Slack app settings.
|
|
571
|
+
- Review Slack app event subscriptions.
|
|
572
|
+
|
|
573
|
+
### Config Reload Not Working
|
|
574
|
+
|
|
575
|
+
- `--watch` requires `--config <path>` (explicit path, not auto-discovery).
|
|
576
|
+
- Check file permissions on the config file.
|
|
577
|
+
- Send `SIGUSR2` manually to test: `kill -USR2 $(pgrep -f visor)`
|
|
578
|
+
|
|
579
|
+
### OOM Kills
|
|
580
|
+
|
|
581
|
+
- Increase memory limits in container spec.
|
|
582
|
+
- Reduce `--max-parallelism` to lower concurrent check count.
|
|
583
|
+
- Check for memory leaks in long-running Slack mode (report at [GitHub Issues](https://github.com/probelabs/visor/issues)).
|
|
@@ -120,6 +120,23 @@ steps:
|
|
|
120
120
|
on: ["pr_opened"]
|
|
121
121
|
tags: ["filesystem", "structure"]
|
|
122
122
|
|
|
123
|
+
# Example 7: Dynamic - Bash config from dependency output (ai_bash_config_js)
|
|
124
|
+
dynamic-bash-from-skills:
|
|
125
|
+
type: ai
|
|
126
|
+
depends_on: [build-config]
|
|
127
|
+
prompt: "Help the user with their request using available commands"
|
|
128
|
+
ai:
|
|
129
|
+
provider: anthropic
|
|
130
|
+
allowBash: true
|
|
131
|
+
bashConfig:
|
|
132
|
+
allow: ['gh:*'] # Static baseline commands
|
|
133
|
+
ai_bash_config_js: |
|
|
134
|
+
// Dynamically extend bash config from build-config output
|
|
135
|
+
// Active skills declare their allowed/disallowed commands
|
|
136
|
+
return outputs['build-config']?.bash_config ?? {};
|
|
137
|
+
on: ["manual"]
|
|
138
|
+
tags: ["dynamic", "skills"]
|
|
139
|
+
|
|
123
140
|
output:
|
|
124
141
|
pr_comment:
|
|
125
142
|
enabled: true
|
|
@@ -457,6 +457,10 @@ export declare const configSchema: {
|
|
|
457
457
|
readonly type: "string";
|
|
458
458
|
readonly description: "JavaScript expression to dynamically compute custom tools for this AI check. Expression has access to: outputs, inputs, pr, files, env, memory Must return an array of tool names (strings) or WorkflowToolReference objects ({ workflow: string, args?: Record<string, unknown> })\n\nExample: ``` const tools = []; if (outputs['route-intent'].intent === 'engineer') { tools.push({ workflow: 'engineer', args: { projects: ['tyk'] } }); } return tools; ```";
|
|
459
459
|
};
|
|
460
|
+
readonly ai_bash_config_js: {
|
|
461
|
+
readonly type: "string";
|
|
462
|
+
readonly description: "JavaScript expression to dynamically compute bash configuration for this AI check. Expression has access to: outputs, inputs, pr, files, env, memory. Must return a BashConfig object with optional allow/deny string arrays.\n\nExample: ``` return outputs['build-config']?.bash_config ?? {}; ```";
|
|
463
|
+
};
|
|
460
464
|
readonly claude_code: {
|
|
461
465
|
readonly $ref: "#/definitions/ClaudeCodeConfig";
|
|
462
466
|
readonly description: "Claude Code configuration (for claude-code type checks)";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/generated/config-schema.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["file:///home/runner/work/visor/visor/src/generated/config-schema.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAo3Ef,CAAC;AACX,eAAe,YAAY,CAAC"}
|