legionio 1.4.79 → 1.4.82
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.
- checksums.yaml +4 -4
- data/.dockerignore +11 -0
- data/.github/workflow-templates/eval-gate.yml +118 -0
- data/.github/workflows/ci-cd.yml +67 -0
- data/CHANGELOG.md +24 -0
- data/Dockerfile +25 -8
- data/deploy/helm/legion/Chart.yaml +6 -0
- data/deploy/helm/legion/templates/_helpers.tpl +16 -0
- data/deploy/helm/legion/templates/deployment-api.yaml +55 -0
- data/deploy/helm/legion/templates/deployment-worker.yaml +46 -0
- data/deploy/helm/legion/templates/hpa-worker.yaml +22 -0
- data/deploy/helm/legion/templates/pdb.yaml +13 -0
- data/deploy/helm/legion/templates/service-api.yaml +15 -0
- data/deploy/helm/legion/templates/serviceaccount.yaml +8 -0
- data/deploy/helm/legion/values.yaml +69 -0
- data/lib/legion/cli/check/privacy_check.rb +86 -0
- data/lib/legion/cli/check_command.rb +46 -0
- data/lib/legion/cli/eval_command.rb +217 -0
- data/lib/legion/cli.rb +10 -1
- data/lib/legion/service.rb +23 -0
- data/lib/legion/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be7718191541e7f3c7d122c0df0f74145ef0ed5aa8e4b8a67396932155e4cccf
|
|
4
|
+
data.tar.gz: 5b4f4e5453fe9667b6424433d8d0ec9cfe524b6f9c35be64f7e4b0edeae1c3f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd10e410a69d1cae47dadbaf02f287857410b44e8f8d305c1cd8b555a44a32d2a6b0a63950a3ee2e54d815ef28a25ee7dc08fd7e78a584ec39698b03385bcac0
|
|
7
|
+
data.tar.gz: ce22d223fd1b67df63878d1e1f4d54273437c01a4359f384dc0658ecf06e1e83481285d2b78afd511ee17fabe75a5cb8342826e5cab6ee63cc49a33ccb3c38e4
|
data/.dockerignore
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# .github/workflow-templates/eval-gate.yml
|
|
2
|
+
#
|
|
3
|
+
# Eval gate workflow template for LegionIO CI/CD pipelines.
|
|
4
|
+
# Copy this file to .github/workflows/eval-gate.yml in your repo and adjust the
|
|
5
|
+
# env vars to match your dataset and threshold requirements.
|
|
6
|
+
#
|
|
7
|
+
# Required secrets:
|
|
8
|
+
# LEGIONIO_BOOTSTRAP_CONFIG (base64-encoded bootstrap JSON, or omit for defaults)
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# - Trigger manually (workflow_dispatch) or on push/PR targeting main
|
|
12
|
+
# - Job exits 0 if avg_score >= threshold, exits 1 and fails the pipeline if below
|
|
13
|
+
|
|
14
|
+
name: Eval Gate
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
push:
|
|
18
|
+
branches: [main]
|
|
19
|
+
pull_request:
|
|
20
|
+
branches: [main]
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
inputs:
|
|
23
|
+
dataset:
|
|
24
|
+
description: 'Dataset name to evaluate'
|
|
25
|
+
required: true
|
|
26
|
+
default: 'default'
|
|
27
|
+
threshold:
|
|
28
|
+
description: 'Pass/fail threshold (0.0 - 1.0)'
|
|
29
|
+
required: false
|
|
30
|
+
default: '0.8'
|
|
31
|
+
evaluator:
|
|
32
|
+
description: 'Evaluator name (leave blank for first builtin template)'
|
|
33
|
+
required: false
|
|
34
|
+
default: ''
|
|
35
|
+
|
|
36
|
+
env:
|
|
37
|
+
DATASET: ${{ github.event.inputs.dataset || 'default' }}
|
|
38
|
+
THRESHOLD: ${{ github.event.inputs.threshold || '0.8' }}
|
|
39
|
+
EVALUATOR: ${{ github.event.inputs.evaluator || '' }}
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
eval-gate:
|
|
43
|
+
name: Eval Gate (${{ env.DATASET }} @ ${{ env.THRESHOLD }})
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
|
|
46
|
+
steps:
|
|
47
|
+
- name: Checkout
|
|
48
|
+
uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Set up Ruby
|
|
51
|
+
uses: ruby/setup-ruby@v1
|
|
52
|
+
with:
|
|
53
|
+
ruby-version: '3.4'
|
|
54
|
+
bundler-cache: true
|
|
55
|
+
|
|
56
|
+
- name: Install Legion
|
|
57
|
+
run: gem install legionio --no-document
|
|
58
|
+
|
|
59
|
+
- name: Bootstrap config (optional)
|
|
60
|
+
if: ${{ secrets.LEGIONIO_BOOTSTRAP_CONFIG != '' }}
|
|
61
|
+
env:
|
|
62
|
+
LEGIONIO_BOOTSTRAP_CONFIG: ${{ secrets.LEGIONIO_BOOTSTRAP_CONFIG }}
|
|
63
|
+
run: echo "Bootstrap config present"
|
|
64
|
+
|
|
65
|
+
- name: Run eval gate
|
|
66
|
+
id: eval
|
|
67
|
+
env:
|
|
68
|
+
LEGIONIO_BOOTSTRAP_CONFIG: ${{ secrets.LEGIONIO_BOOTSTRAP_CONFIG }}
|
|
69
|
+
run: |
|
|
70
|
+
EVAL_ARGS="--dataset $DATASET --threshold $THRESHOLD --exit-code --json"
|
|
71
|
+
if [ -n "$EVALUATOR" ]; then
|
|
72
|
+
EVAL_ARGS="$EVAL_ARGS --evaluator $EVALUATOR"
|
|
73
|
+
fi
|
|
74
|
+
legion eval run $EVAL_ARGS | tee eval-report.json
|
|
75
|
+
|
|
76
|
+
- name: Upload eval report
|
|
77
|
+
if: always()
|
|
78
|
+
uses: actions/upload-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: eval-report-${{ github.run_number }}
|
|
81
|
+
path: eval-report.json
|
|
82
|
+
retention-days: 30
|
|
83
|
+
|
|
84
|
+
- name: Annotate PR with eval results
|
|
85
|
+
if: github.event_name == 'pull_request' && always()
|
|
86
|
+
uses: actions/github-script@v7
|
|
87
|
+
with:
|
|
88
|
+
script: |
|
|
89
|
+
const fs = require('fs');
|
|
90
|
+
let report;
|
|
91
|
+
try {
|
|
92
|
+
report = JSON.parse(fs.readFileSync('eval-report.json', 'utf8'));
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.log('Could not parse eval report:', e.message);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const gate = report.passed ? 'PASSED' : 'FAILED';
|
|
98
|
+
const score = (report.avg_score || 0).toFixed(3);
|
|
99
|
+
const thresh = report.threshold || 0;
|
|
100
|
+
const body = [
|
|
101
|
+
`## Eval Gate: ${gate}`,
|
|
102
|
+
'',
|
|
103
|
+
`| Metric | Value |`,
|
|
104
|
+
`|--------|-------|`,
|
|
105
|
+
`| Dataset | \`${report.dataset}\` |`,
|
|
106
|
+
`| Evaluator | \`${report.evaluator}\` |`,
|
|
107
|
+
`| Avg Score | ${score} |`,
|
|
108
|
+
`| Threshold | ${thresh} |`,
|
|
109
|
+
`| Total Rows | ${report.summary?.total ?? 'N/A'} |`,
|
|
110
|
+
`| Passed | ${report.summary?.passed ?? 'N/A'} |`,
|
|
111
|
+
`| Failed | ${report.summary?.failed ?? 'N/A'} |`,
|
|
112
|
+
].join('\n');
|
|
113
|
+
github.rest.issues.createComment({
|
|
114
|
+
issue_number: context.issue.number,
|
|
115
|
+
owner: context.repo.owner,
|
|
116
|
+
repo: context.repo.repo,
|
|
117
|
+
body: body,
|
|
118
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: CI/CD
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Test
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
services:
|
|
13
|
+
rabbitmq:
|
|
14
|
+
image: rabbitmq:3.13-management
|
|
15
|
+
ports: ['5672:5672']
|
|
16
|
+
postgres:
|
|
17
|
+
image: postgres:16
|
|
18
|
+
env:
|
|
19
|
+
POSTGRES_PASSWORD: test
|
|
20
|
+
POSTGRES_DB: legion_test
|
|
21
|
+
ports: ['5432:5432']
|
|
22
|
+
options: >-
|
|
23
|
+
--health-cmd pg_isready
|
|
24
|
+
--health-interval 10s
|
|
25
|
+
--health-timeout 5s
|
|
26
|
+
--health-retries 5
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: ruby/setup-ruby@v1
|
|
30
|
+
with:
|
|
31
|
+
ruby-version: '3.4'
|
|
32
|
+
bundler-cache: true
|
|
33
|
+
- run: bundle install && bundle exec rspec
|
|
34
|
+
- run: bundle exec rubocop
|
|
35
|
+
|
|
36
|
+
build:
|
|
37
|
+
name: Build Image
|
|
38
|
+
needs: test
|
|
39
|
+
if: github.ref == 'refs/heads/main'
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
permissions:
|
|
42
|
+
packages: write
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: docker/setup-buildx-action@v3
|
|
46
|
+
- uses: docker/login-action@v3
|
|
47
|
+
with:
|
|
48
|
+
registry: ghcr.io
|
|
49
|
+
username: ${{ github.actor }}
|
|
50
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
- uses: docker/build-push-action@v5
|
|
52
|
+
with:
|
|
53
|
+
context: .
|
|
54
|
+
push: true
|
|
55
|
+
tags: |
|
|
56
|
+
ghcr.io/legionio/legion:${{ github.sha }}
|
|
57
|
+
ghcr.io/legionio/legion:latest
|
|
58
|
+
cache-from: type=gha
|
|
59
|
+
cache-to: type=gha,mode=max
|
|
60
|
+
|
|
61
|
+
helm-lint:
|
|
62
|
+
name: Helm Lint
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
- uses: azure/setup-helm@v3
|
|
67
|
+
- run: helm lint deploy/helm/legion
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.82] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `legion check --privacy` command: verifies enterprise privacy mode (flag set, no cloud API keys, external endpoints unreachable)
|
|
7
|
+
- `PrivacyCheck` class with three probes: flag_set, no_cloud_keys, no_external_endpoints
|
|
8
|
+
- `Legion::Service.log_privacy_mode_status` logs enterprise privacy state at startup
|
|
9
|
+
|
|
10
|
+
## [1.4.81] - 2026-03-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `legion eval experiments` subcommand: list all experiment runs with status and summary
|
|
14
|
+
- `legion eval promote --experiment NAME --tag TAG` subcommand: tag a prompt version for production via lex-prompt
|
|
15
|
+
- `legion eval compare --run1 NAME --run2 NAME` subcommand: side-by-side diff of two experiment runs
|
|
16
|
+
- `require_prompt!` guard for lex-prompt extension availability
|
|
17
|
+
|
|
18
|
+
## [1.4.80] - 2026-03-20
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `legion eval run` CLI subcommand for CI/CD threshold-based eval gating
|
|
22
|
+
- `--dataset`, `--threshold`, `--evaluator`, `--exit-code` options on `eval run`
|
|
23
|
+
- JSON report output to stdout with per-row scores, summary, and timestamp
|
|
24
|
+
- `.github/workflow-templates/eval-gate.yml` reusable GitHub Actions workflow template
|
|
25
|
+
- PR annotation step in workflow template for inline eval result comments
|
|
26
|
+
|
|
3
27
|
## [1.4.79] - 2026-03-20
|
|
4
28
|
|
|
5
29
|
### Added
|
data/Dockerfile
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# Build stage
|
|
2
|
+
FROM ruby:3.4-slim AS builder
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
RUN apt-get update && \
|
|
5
|
+
apt-get install -y --no-install-recommends build-essential libpq-dev git && \
|
|
6
|
+
rm -rf /var/lib/apt/lists/*
|
|
7
|
+
COPY Gemfile Gemfile.lock ./
|
|
8
|
+
RUN bundle config set --local deployment true && \
|
|
9
|
+
bundle config set --local without 'development test' && \
|
|
10
|
+
bundle install --jobs 4 --retry 3
|
|
11
|
+
COPY . .
|
|
3
12
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
# Runtime stage
|
|
14
|
+
FROM ruby:3.4-slim AS runtime
|
|
15
|
+
RUN apt-get update && \
|
|
16
|
+
apt-get install -y --no-install-recommends libpq5 curl && \
|
|
17
|
+
rm -rf /var/lib/apt/lists/* && \
|
|
18
|
+
groupadd -r legion && useradd -r -g legion -d /app -s /sbin/nologin legion
|
|
19
|
+
WORKDIR /app
|
|
20
|
+
COPY --from=builder --chown=legion:legion /app /app
|
|
21
|
+
USER legion
|
|
22
|
+
EXPOSE 4567
|
|
23
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
24
|
+
CMD curl -sf http://localhost:4567/api/health || exit 1
|
|
25
|
+
ENTRYPOINT ["bundle", "exec"]
|
|
26
|
+
CMD ["legion", "start"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{- define "legion.fullname" -}}
|
|
2
|
+
{{- .Release.Name }}-legion
|
|
3
|
+
{{- end }}
|
|
4
|
+
|
|
5
|
+
{{- define "legion.labels" -}}
|
|
6
|
+
app.kubernetes.io/name: legion
|
|
7
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
8
|
+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
9
|
+
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
10
|
+
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
|
|
11
|
+
{{- end }}
|
|
12
|
+
|
|
13
|
+
{{- define "legion.selectorLabels" -}}
|
|
14
|
+
app.kubernetes.io/name: legion
|
|
15
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
16
|
+
{{- end }}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ include "legion.fullname" . }}-api
|
|
5
|
+
labels:
|
|
6
|
+
{{- include "legion.labels" . | nindent 4 }}
|
|
7
|
+
app.kubernetes.io/component: api
|
|
8
|
+
spec:
|
|
9
|
+
replicas: {{ .Values.api.replicas }}
|
|
10
|
+
selector:
|
|
11
|
+
matchLabels:
|
|
12
|
+
{{- include "legion.selectorLabels" . | nindent 6 }}
|
|
13
|
+
app.kubernetes.io/component: api
|
|
14
|
+
template:
|
|
15
|
+
metadata:
|
|
16
|
+
labels:
|
|
17
|
+
{{- include "legion.selectorLabels" . | nindent 8 }}
|
|
18
|
+
app.kubernetes.io/component: api
|
|
19
|
+
spec:
|
|
20
|
+
serviceAccountName: {{ .Values.serviceAccount.name }}
|
|
21
|
+
containers:
|
|
22
|
+
- name: api
|
|
23
|
+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
|
24
|
+
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
25
|
+
command: ["bundle", "exec", "legion", "api"]
|
|
26
|
+
ports:
|
|
27
|
+
- containerPort: {{ .Values.api.port }}
|
|
28
|
+
protocol: TCP
|
|
29
|
+
livenessProbe:
|
|
30
|
+
httpGet:
|
|
31
|
+
path: /api/health
|
|
32
|
+
port: {{ .Values.api.port }}
|
|
33
|
+
initialDelaySeconds: 10
|
|
34
|
+
periodSeconds: 30
|
|
35
|
+
readinessProbe:
|
|
36
|
+
httpGet:
|
|
37
|
+
path: /api/health
|
|
38
|
+
port: {{ .Values.api.port }}
|
|
39
|
+
initialDelaySeconds: 5
|
|
40
|
+
periodSeconds: 10
|
|
41
|
+
resources:
|
|
42
|
+
{{- toYaml .Values.api.resources | nindent 12 }}
|
|
43
|
+
env:
|
|
44
|
+
- name: LEGION_TRANSPORT_HOST
|
|
45
|
+
value: {{ .Values.rabbitmq.host | quote }}
|
|
46
|
+
- name: LEGION_DATA_URL
|
|
47
|
+
value: "postgres://$(DB_USER):$(DB_PASS)@{{ .Values.postgresql.host }}:{{ .Values.postgresql.port }}/{{ .Values.postgresql.database }}"
|
|
48
|
+
{{- with .Values.api.env }}
|
|
49
|
+
{{- toYaml . | nindent 12 }}
|
|
50
|
+
{{- end }}
|
|
51
|
+
envFrom:
|
|
52
|
+
- secretRef:
|
|
53
|
+
name: {{ .Values.rabbitmq.existingSecret }}
|
|
54
|
+
- secretRef:
|
|
55
|
+
name: {{ .Values.postgresql.existingSecret }}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ include "legion.fullname" . }}-worker
|
|
5
|
+
labels:
|
|
6
|
+
{{- include "legion.labels" . | nindent 4 }}
|
|
7
|
+
app.kubernetes.io/component: worker
|
|
8
|
+
spec:
|
|
9
|
+
replicas: {{ .Values.worker.replicas }}
|
|
10
|
+
selector:
|
|
11
|
+
matchLabels:
|
|
12
|
+
{{- include "legion.selectorLabels" . | nindent 6 }}
|
|
13
|
+
app.kubernetes.io/component: worker
|
|
14
|
+
template:
|
|
15
|
+
metadata:
|
|
16
|
+
labels:
|
|
17
|
+
{{- include "legion.selectorLabels" . | nindent 8 }}
|
|
18
|
+
app.kubernetes.io/component: worker
|
|
19
|
+
spec:
|
|
20
|
+
serviceAccountName: {{ .Values.serviceAccount.name }}
|
|
21
|
+
containers:
|
|
22
|
+
- name: worker
|
|
23
|
+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
|
24
|
+
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
25
|
+
command: ["bundle", "exec", "legion", "start"]
|
|
26
|
+
resources:
|
|
27
|
+
{{- toYaml .Values.worker.resources | nindent 12 }}
|
|
28
|
+
env:
|
|
29
|
+
- name: LEGION_TRANSPORT_HOST
|
|
30
|
+
value: {{ .Values.rabbitmq.host | quote }}
|
|
31
|
+
- name: LEGION_TRANSPORT_PORT
|
|
32
|
+
value: {{ .Values.rabbitmq.port | quote }}
|
|
33
|
+
- name: LEGION_DATA_URL
|
|
34
|
+
value: "postgres://$(DB_USER):$(DB_PASS)@{{ .Values.postgresql.host }}:{{ .Values.postgresql.port }}/{{ .Values.postgresql.database }}"
|
|
35
|
+
{{- with .Values.worker.env }}
|
|
36
|
+
{{- toYaml . | nindent 12 }}
|
|
37
|
+
{{- end }}
|
|
38
|
+
envFrom:
|
|
39
|
+
- secretRef:
|
|
40
|
+
name: {{ .Values.rabbitmq.existingSecret }}
|
|
41
|
+
- secretRef:
|
|
42
|
+
name: {{ .Values.postgresql.existingSecret }}
|
|
43
|
+
{{- with .Values.nodeSelector }}
|
|
44
|
+
nodeSelector:
|
|
45
|
+
{{- toYaml . | nindent 8 }}
|
|
46
|
+
{{- end }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{{- if .Values.worker.hpa.enabled }}
|
|
2
|
+
apiVersion: autoscaling/v2
|
|
3
|
+
kind: HorizontalPodAutoscaler
|
|
4
|
+
metadata:
|
|
5
|
+
name: {{ include "legion.fullname" . }}-worker
|
|
6
|
+
labels:
|
|
7
|
+
{{- include "legion.labels" . | nindent 4 }}
|
|
8
|
+
spec:
|
|
9
|
+
scaleTargetRef:
|
|
10
|
+
apiVersion: apps/v1
|
|
11
|
+
kind: Deployment
|
|
12
|
+
name: {{ include "legion.fullname" . }}-worker
|
|
13
|
+
minReplicas: {{ .Values.worker.hpa.minReplicas }}
|
|
14
|
+
maxReplicas: {{ .Values.worker.hpa.maxReplicas }}
|
|
15
|
+
metrics:
|
|
16
|
+
- type: Resource
|
|
17
|
+
resource:
|
|
18
|
+
name: cpu
|
|
19
|
+
target:
|
|
20
|
+
type: Utilization
|
|
21
|
+
averageUtilization: {{ .Values.worker.hpa.targetCPUUtilization }}
|
|
22
|
+
{{- end }}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{{- if .Values.podDisruptionBudget.enabled }}
|
|
2
|
+
apiVersion: policy/v1
|
|
3
|
+
kind: PodDisruptionBudget
|
|
4
|
+
metadata:
|
|
5
|
+
name: {{ include "legion.fullname" . }}
|
|
6
|
+
labels:
|
|
7
|
+
{{- include "legion.labels" . | nindent 4 }}
|
|
8
|
+
spec:
|
|
9
|
+
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
|
10
|
+
selector:
|
|
11
|
+
matchLabels:
|
|
12
|
+
{{- include "legion.selectorLabels" . | nindent 6 }}
|
|
13
|
+
{{- end }}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Service
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ include "legion.fullname" . }}-api
|
|
5
|
+
labels:
|
|
6
|
+
{{- include "legion.labels" . | nindent 4 }}
|
|
7
|
+
spec:
|
|
8
|
+
type: {{ .Values.api.service.type }}
|
|
9
|
+
ports:
|
|
10
|
+
- port: {{ .Values.api.port }}
|
|
11
|
+
targetPort: {{ .Values.api.port }}
|
|
12
|
+
protocol: TCP
|
|
13
|
+
selector:
|
|
14
|
+
{{- include "legion.selectorLabels" . | nindent 4 }}
|
|
15
|
+
app.kubernetes.io/component: api
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
image:
|
|
2
|
+
repository: ghcr.io/legionio/legion
|
|
3
|
+
tag: latest
|
|
4
|
+
pullPolicy: IfNotPresent
|
|
5
|
+
|
|
6
|
+
worker:
|
|
7
|
+
replicas: 2
|
|
8
|
+
resources:
|
|
9
|
+
requests:
|
|
10
|
+
cpu: 250m
|
|
11
|
+
memory: 256Mi
|
|
12
|
+
limits:
|
|
13
|
+
cpu: 1000m
|
|
14
|
+
memory: 512Mi
|
|
15
|
+
hpa:
|
|
16
|
+
enabled: false
|
|
17
|
+
minReplicas: 2
|
|
18
|
+
maxReplicas: 20
|
|
19
|
+
targetCPUUtilization: 70
|
|
20
|
+
env: []
|
|
21
|
+
|
|
22
|
+
api:
|
|
23
|
+
replicas: 2
|
|
24
|
+
port: 4567
|
|
25
|
+
resources:
|
|
26
|
+
requests:
|
|
27
|
+
cpu: 100m
|
|
28
|
+
memory: 128Mi
|
|
29
|
+
limits:
|
|
30
|
+
cpu: 500m
|
|
31
|
+
memory: 256Mi
|
|
32
|
+
service:
|
|
33
|
+
type: ClusterIP
|
|
34
|
+
ingress:
|
|
35
|
+
enabled: false
|
|
36
|
+
env: []
|
|
37
|
+
|
|
38
|
+
rabbitmq:
|
|
39
|
+
host: rabbitmq
|
|
40
|
+
port: 5672
|
|
41
|
+
vhost: /
|
|
42
|
+
existingSecret: legion-rabbitmq
|
|
43
|
+
|
|
44
|
+
postgresql:
|
|
45
|
+
host: postgresql
|
|
46
|
+
port: 5432
|
|
47
|
+
database: legion
|
|
48
|
+
existingSecret: legion-postgresql
|
|
49
|
+
|
|
50
|
+
redis:
|
|
51
|
+
host: redis
|
|
52
|
+
port: 6379
|
|
53
|
+
|
|
54
|
+
vault:
|
|
55
|
+
enabled: false
|
|
56
|
+
address: ""
|
|
57
|
+
role: legion
|
|
58
|
+
|
|
59
|
+
serviceAccount:
|
|
60
|
+
create: true
|
|
61
|
+
name: legion
|
|
62
|
+
|
|
63
|
+
podDisruptionBudget:
|
|
64
|
+
enabled: true
|
|
65
|
+
minAvailable: 1
|
|
66
|
+
|
|
67
|
+
nodeSelector: {}
|
|
68
|
+
tolerations: []
|
|
69
|
+
affinity: {}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module CLI
|
|
5
|
+
module Check
|
|
6
|
+
class PrivacyCheck
|
|
7
|
+
CLOUD_PROVIDERS = %i[bedrock anthropic openai gemini azure].freeze
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
@results = {}
|
|
11
|
+
@results[:flag_set] = check_flag_set
|
|
12
|
+
@results[:no_cloud_keys] = check_no_cloud_keys
|
|
13
|
+
@results[:no_external_endpoints] = check_no_external_endpoints
|
|
14
|
+
@results
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def overall_pass?
|
|
18
|
+
run.values.all? { |v| v == :pass }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def check_flag_set
|
|
24
|
+
if settings_loaded? && Legion::Settings.enterprise_privacy?
|
|
25
|
+
:pass
|
|
26
|
+
else
|
|
27
|
+
:fail
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check_no_cloud_keys
|
|
32
|
+
llm = Legion::Settings[:llm]
|
|
33
|
+
return :pass unless llm.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
providers = (llm[:providers] || llm['providers'] || {}).transform_keys(&:to_sym)
|
|
36
|
+
CLOUD_PROVIDERS.each do |provider|
|
|
37
|
+
cfg = providers[provider]
|
|
38
|
+
return :fail if raw_credential?(cfg)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
:pass
|
|
42
|
+
rescue StandardError
|
|
43
|
+
:skip
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def raw_credential?(cfg)
|
|
47
|
+
return false unless cfg.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
key = cfg[:api_key] || cfg['api_key'] ||
|
|
50
|
+
cfg[:bearer_token] || cfg['bearer_token'] ||
|
|
51
|
+
cfg[:secret_key] || cfg['secret_key']
|
|
52
|
+
|
|
53
|
+
key.is_a?(String) && !key.empty? && !key.start_with?('env://', 'vault://')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def check_no_external_endpoints
|
|
57
|
+
endpoints = [
|
|
58
|
+
['api.anthropic.com', 443],
|
|
59
|
+
['api.openai.com', 443],
|
|
60
|
+
['generativelanguage.googleapis.com', 443]
|
|
61
|
+
]
|
|
62
|
+
endpoints.each do |host, port|
|
|
63
|
+
return :fail if tcp_reachable?(host, port)
|
|
64
|
+
end
|
|
65
|
+
:pass
|
|
66
|
+
rescue StandardError
|
|
67
|
+
:skip
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def tcp_reachable?(host, port)
|
|
71
|
+
socket = ::TCPSocket.new(host, port)
|
|
72
|
+
socket.close
|
|
73
|
+
true
|
|
74
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError, Errno::ENETUNREACH
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def settings_loaded?
|
|
79
|
+
defined?(Legion::Settings) && Legion::Settings.respond_to?(:enterprise_privacy?)
|
|
80
|
+
rescue StandardError
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -17,7 +17,53 @@ module Legion
|
|
|
17
17
|
api: :transport
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
|
+
autoload :PrivacyCheck, 'legion/cli/check/privacy_check'
|
|
21
|
+
|
|
22
|
+
PROBE_LABELS = {
|
|
23
|
+
flag_set: 'Privacy flag set',
|
|
24
|
+
no_cloud_keys: 'No cloud API keys configured',
|
|
25
|
+
no_external_endpoints: 'External endpoints unreachable'
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
20
28
|
class << self
|
|
29
|
+
def run_privacy(formatter, options)
|
|
30
|
+
require 'legion/settings'
|
|
31
|
+
dir = Connection.send(:resolve_config_dir)
|
|
32
|
+
Legion::Settings.load(config_dir: dir)
|
|
33
|
+
|
|
34
|
+
checker = PrivacyCheck.new
|
|
35
|
+
results = checker.run
|
|
36
|
+
|
|
37
|
+
if options[:json]
|
|
38
|
+
formatter.json({ results: results, overall: checker.overall_pass? ? 'pass' : 'fail' })
|
|
39
|
+
return checker.overall_pass? ? 0 : 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
formatter.header('Enterprise Privacy Mode Check')
|
|
43
|
+
formatter.spacer
|
|
44
|
+
|
|
45
|
+
results.each do |probe, status|
|
|
46
|
+
label = PROBE_LABELS.fetch(probe, probe.to_s).ljust(36)
|
|
47
|
+
case status
|
|
48
|
+
when :pass
|
|
49
|
+
puts " #{label}#{formatter.colorize('pass', :green)}"
|
|
50
|
+
when :fail
|
|
51
|
+
puts " #{label}#{formatter.colorize('FAIL', :red)}"
|
|
52
|
+
when :skip
|
|
53
|
+
puts " #{label}#{formatter.colorize('skip', :yellow)}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
formatter.spacer
|
|
58
|
+
if checker.overall_pass?
|
|
59
|
+
formatter.success('Privacy mode fully engaged')
|
|
60
|
+
else
|
|
61
|
+
formatter.error('Privacy mode check failed — see items above')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
checker.overall_pass? ? 0 : 1
|
|
65
|
+
end
|
|
66
|
+
|
|
21
67
|
def run(formatter, options)
|
|
22
68
|
level = if options[:full]
|
|
23
69
|
:full
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module CLI
|
|
7
|
+
class Eval < Thor
|
|
8
|
+
def self.exit_on_failure?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
13
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
14
|
+
class_option :verbose, type: :boolean, default: false, aliases: ['-V'], desc: 'Verbose logging'
|
|
15
|
+
class_option :config_dir, type: :string, desc: 'Config directory path'
|
|
16
|
+
|
|
17
|
+
desc 'run', 'Run eval against a dataset and gate on a threshold'
|
|
18
|
+
map 'run' => :execute
|
|
19
|
+
option :dataset, type: :string, required: true, aliases: '-d', desc: 'Dataset name'
|
|
20
|
+
option :threshold, type: :numeric, default: 0.8, aliases: '-t', desc: 'Pass/fail threshold (0.0-1.0)'
|
|
21
|
+
option :evaluator, type: :string, default: nil, aliases: '-e', desc: 'Evaluator name'
|
|
22
|
+
option :exit_code, type: :boolean, default: false, desc: 'Exit 1 if gate fails (for CI use)'
|
|
23
|
+
def execute
|
|
24
|
+
setup_connection
|
|
25
|
+
require_eval!
|
|
26
|
+
require_dataset!
|
|
27
|
+
|
|
28
|
+
rows = fetch_dataset_rows(options[:dataset])
|
|
29
|
+
report = run_evaluations(rows)
|
|
30
|
+
|
|
31
|
+
avg_score = report.dig(:summary, :avg_score) || 0.0
|
|
32
|
+
passed = avg_score >= options[:threshold]
|
|
33
|
+
|
|
34
|
+
ci_report = build_ci_report(report, avg_score, passed)
|
|
35
|
+
|
|
36
|
+
if options[:json]
|
|
37
|
+
formatter.json(ci_report)
|
|
38
|
+
else
|
|
39
|
+
render_human_report(ci_report, avg_score, passed)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
exit(1) if options[:exit_code] && !passed
|
|
43
|
+
ensure
|
|
44
|
+
Connection.shutdown
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc 'experiments', 'List all tracked experiments'
|
|
48
|
+
def experiments
|
|
49
|
+
setup_connection
|
|
50
|
+
require_dataset!
|
|
51
|
+
|
|
52
|
+
client = Legion::Extensions::Dataset::Client.new
|
|
53
|
+
rows = client.list_experiments
|
|
54
|
+
out = formatter
|
|
55
|
+
|
|
56
|
+
if rows.empty?
|
|
57
|
+
out.warn('no experiments found')
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if options[:json]
|
|
62
|
+
out.json(experiments: rows)
|
|
63
|
+
else
|
|
64
|
+
out.header('Experiments')
|
|
65
|
+
out.spacer
|
|
66
|
+
table_rows = rows.map do |r|
|
|
67
|
+
[r[:id].to_s, r[:name].to_s, r[:status].to_s, r[:created_at].to_s, r[:summary].to_s[0, 60]]
|
|
68
|
+
end
|
|
69
|
+
out.table(%w[id name status created summary], table_rows)
|
|
70
|
+
end
|
|
71
|
+
ensure
|
|
72
|
+
Connection.shutdown
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc 'promote', 'Tag a prompt version from a passing experiment for production'
|
|
76
|
+
option :experiment, type: :string, required: true, aliases: '-e', desc: 'Experiment name'
|
|
77
|
+
option :tag, type: :string, required: true, aliases: '-t', desc: 'Tag to apply (e.g. production)'
|
|
78
|
+
def promote
|
|
79
|
+
setup_connection
|
|
80
|
+
require_dataset!
|
|
81
|
+
require_prompt!
|
|
82
|
+
|
|
83
|
+
dataset_client = Legion::Extensions::Dataset::Client.new
|
|
84
|
+
experiment = dataset_client.get_experiment(name: options[:experiment])
|
|
85
|
+
raise CLI::Error, "Experiment '#{options[:experiment]}' not found" if experiment.nil?
|
|
86
|
+
raise CLI::Error, "Experiment '#{options[:experiment]}' has no prompt linked" if experiment[:prompt_name].nil?
|
|
87
|
+
|
|
88
|
+
prompt_client = Legion::Extensions::Prompt::Client.new
|
|
89
|
+
result = prompt_client.tag_prompt(
|
|
90
|
+
name: experiment[:prompt_name],
|
|
91
|
+
tag: options[:tag],
|
|
92
|
+
version: experiment[:prompt_version]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
out = formatter
|
|
96
|
+
if options[:json]
|
|
97
|
+
out.json(result)
|
|
98
|
+
else
|
|
99
|
+
out.success("Tagged prompt '#{experiment[:prompt_name]}' v#{experiment[:prompt_version]} as '#{options[:tag]}'")
|
|
100
|
+
end
|
|
101
|
+
ensure
|
|
102
|
+
Connection.shutdown
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
desc 'compare', 'Compare two experiment runs side by side'
|
|
106
|
+
option :run1, type: :string, required: true, desc: 'First experiment name'
|
|
107
|
+
option :run2, type: :string, required: true, desc: 'Second experiment name'
|
|
108
|
+
def compare
|
|
109
|
+
setup_connection
|
|
110
|
+
require_dataset!
|
|
111
|
+
|
|
112
|
+
client = Legion::Extensions::Dataset::Client.new
|
|
113
|
+
diff = client.compare_experiments(exp1_name: options[:run1], exp2_name: options[:run2])
|
|
114
|
+
raise CLI::Error, 'One or both experiments not found' if diff[:error]
|
|
115
|
+
|
|
116
|
+
out = formatter
|
|
117
|
+
if options[:json]
|
|
118
|
+
out.json(diff)
|
|
119
|
+
else
|
|
120
|
+
out.header("Compare: #{diff[:exp1]} vs #{diff[:exp2]}")
|
|
121
|
+
out.spacer
|
|
122
|
+
table_rows = [
|
|
123
|
+
['Rows compared', diff[:rows_compared].to_s],
|
|
124
|
+
['Regressions', diff[:regression_count].to_s],
|
|
125
|
+
['Improvements', diff[:improvement_count].to_s]
|
|
126
|
+
]
|
|
127
|
+
out.table(%w[metric value], table_rows)
|
|
128
|
+
end
|
|
129
|
+
ensure
|
|
130
|
+
Connection.shutdown
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
no_commands do # rubocop:disable Metrics/BlockLength
|
|
134
|
+
def formatter
|
|
135
|
+
@formatter ||= Output::Formatter.new(
|
|
136
|
+
json: options[:json],
|
|
137
|
+
color: !options[:no_color]
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def setup_connection
|
|
142
|
+
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
143
|
+
Connection.log_level = options[:verbose] ? 'debug' : 'error'
|
|
144
|
+
Connection.ensure_data
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def require_eval!
|
|
148
|
+
return if defined?(Legion::Extensions::Eval::Client)
|
|
149
|
+
|
|
150
|
+
raise CLI::Error, 'lex-eval extension is not loaded. Install and enable it first.'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def require_dataset!
|
|
154
|
+
return if defined?(Legion::Extensions::Dataset::Client)
|
|
155
|
+
|
|
156
|
+
raise CLI::Error, 'lex-dataset extension is not loaded. Install and enable it first.'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def require_prompt!
|
|
160
|
+
return if defined?(Legion::Extensions::Prompt::Client)
|
|
161
|
+
|
|
162
|
+
raise CLI::Error, 'lex-prompt extension is not loaded. Install and enable it first.'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def fetch_dataset_rows(name)
|
|
166
|
+
client = Legion::Extensions::Dataset::Client.new
|
|
167
|
+
result = client.get_dataset(name: name)
|
|
168
|
+
raise CLI::Error, "Dataset '#{name}' not found" if result[:error]
|
|
169
|
+
|
|
170
|
+
result[:rows].map do |r|
|
|
171
|
+
{ input: r[:input], output: r[:input], expected: r[:expected_output] }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def run_evaluations(rows)
|
|
176
|
+
Legion::Extensions::Eval::Client.new.run_evaluation(inputs: rows)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_ci_report(report, avg_score, passed)
|
|
180
|
+
{
|
|
181
|
+
dataset: options[:dataset],
|
|
182
|
+
evaluator: report[:evaluator],
|
|
183
|
+
threshold: options[:threshold],
|
|
184
|
+
avg_score: avg_score,
|
|
185
|
+
passed: passed,
|
|
186
|
+
summary: report[:summary],
|
|
187
|
+
results: report[:results],
|
|
188
|
+
timestamp: Time.now.utc.iso8601
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def render_human_report(report, avg_score, passed)
|
|
193
|
+
out = formatter
|
|
194
|
+
out.header("Eval Gate: #{report[:dataset]}")
|
|
195
|
+
out.spacer
|
|
196
|
+
out.detail({
|
|
197
|
+
dataset: report[:dataset],
|
|
198
|
+
evaluator: report[:evaluator],
|
|
199
|
+
total: report.dig(:summary, :total),
|
|
200
|
+
passed: report.dig(:summary, :passed),
|
|
201
|
+
failed: report.dig(:summary, :failed),
|
|
202
|
+
avg_score: format('%.3f', avg_score),
|
|
203
|
+
threshold: report[:threshold],
|
|
204
|
+
gate: passed ? 'PASSED' : 'FAILED'
|
|
205
|
+
})
|
|
206
|
+
out.spacer
|
|
207
|
+
|
|
208
|
+
if passed
|
|
209
|
+
out.success("Gate PASSED (avg_score=#{format('%.3f', avg_score)} >= threshold=#{report[:threshold]})")
|
|
210
|
+
else
|
|
211
|
+
out.warn("Gate FAILED (avg_score=#{format('%.3f', avg_score)} < threshold=#{report[:threshold]})")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -36,6 +36,7 @@ module Legion
|
|
|
36
36
|
autoload :Rbac, 'legion/cli/rbac_command'
|
|
37
37
|
autoload :Audit, 'legion/cli/audit_command'
|
|
38
38
|
autoload :Detect, 'legion/cli/detect_command'
|
|
39
|
+
autoload :Eval, 'legion/cli/eval_command'
|
|
39
40
|
autoload :Update, 'legion/cli/update_command'
|
|
40
41
|
autoload :Init, 'legion/cli/init_command'
|
|
41
42
|
autoload :Skill, 'legion/cli/skill_command'
|
|
@@ -137,8 +138,13 @@ module Legion
|
|
|
137
138
|
DESC
|
|
138
139
|
option :extensions, type: :boolean, default: false, desc: 'Also load extensions'
|
|
139
140
|
option :full, type: :boolean, default: false, desc: 'Full boot cycle (extensions + API)'
|
|
141
|
+
option :privacy, type: :boolean, default: false, desc: 'Verify enterprise privacy mode'
|
|
140
142
|
def check
|
|
141
|
-
exit_code =
|
|
143
|
+
exit_code = if options[:privacy]
|
|
144
|
+
Legion::CLI::Check.run_privacy(formatter, options)
|
|
145
|
+
else
|
|
146
|
+
Legion::CLI::Check.run(formatter, options)
|
|
147
|
+
end
|
|
142
148
|
exit(exit_code) if exit_code != 0
|
|
143
149
|
end
|
|
144
150
|
|
|
@@ -242,6 +248,9 @@ module Legion
|
|
|
242
248
|
desc 'tty', 'Rich terminal UI (onboarding, AI chat, dashboard)'
|
|
243
249
|
subcommand 'tty', Legion::CLI::Tty
|
|
244
250
|
|
|
251
|
+
desc 'eval SUBCOMMAND', 'Eval gating and experiment management'
|
|
252
|
+
subcommand 'eval', Legion::CLI::Eval
|
|
253
|
+
|
|
245
254
|
desc 'observe SUBCOMMAND', 'MCP tool observation stats'
|
|
246
255
|
subcommand 'observe', Legion::CLI::ObserveCommand
|
|
247
256
|
|
data/lib/legion/service.rb
CHANGED
|
@@ -141,6 +141,7 @@ module Legion
|
|
|
141
141
|
Legion::Settings.load(config_dir: config_directory)
|
|
142
142
|
Legion::Readiness.mark_ready(:settings)
|
|
143
143
|
Legion::Logging.info('Legion::Settings Loaded')
|
|
144
|
+
self.class.log_privacy_mode_status
|
|
144
145
|
end
|
|
145
146
|
|
|
146
147
|
def apply_cli_overrides(http_port: nil)
|
|
@@ -402,5 +403,27 @@ module Legion
|
|
|
402
403
|
require 'legion/runner'
|
|
403
404
|
Legion::Extensions.hook_extensions
|
|
404
405
|
end
|
|
406
|
+
|
|
407
|
+
def self.log_privacy_mode_status
|
|
408
|
+
privacy = if Legion.const_defined?('Settings') && Legion::Settings.respond_to?(:enterprise_privacy?)
|
|
409
|
+
Legion::Settings.enterprise_privacy?
|
|
410
|
+
else
|
|
411
|
+
ENV['LEGION_ENTERPRISE_PRIVACY'] == 'true'
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
message = if privacy
|
|
415
|
+
'enterprise_data_privacy enabled: cloud LLM blocked, telemetry suppressed'
|
|
416
|
+
else
|
|
417
|
+
'enterprise_data_privacy disabled: all tiers available'
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
if Legion.const_defined?('Logging')
|
|
421
|
+
Legion::Logging.info(message)
|
|
422
|
+
else
|
|
423
|
+
$stdout.puts "[Legion] #{message}"
|
|
424
|
+
end
|
|
425
|
+
rescue StandardError
|
|
426
|
+
nil
|
|
427
|
+
end
|
|
405
428
|
end
|
|
406
429
|
end
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.82
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -313,6 +313,9 @@ executables:
|
|
|
313
313
|
extensions: []
|
|
314
314
|
extra_rdoc_files: []
|
|
315
315
|
files:
|
|
316
|
+
- ".dockerignore"
|
|
317
|
+
- ".github/workflow-templates/eval-gate.yml"
|
|
318
|
+
- ".github/workflows/ci-cd.yml"
|
|
316
319
|
- ".github/workflows/ci.yml"
|
|
317
320
|
- ".gitignore"
|
|
318
321
|
- ".rubocop.yml"
|
|
@@ -326,6 +329,15 @@ files:
|
|
|
326
329
|
- completions/_legionio
|
|
327
330
|
- completions/legion.bash
|
|
328
331
|
- completions/legionio.bash
|
|
332
|
+
- deploy/helm/legion/Chart.yaml
|
|
333
|
+
- deploy/helm/legion/templates/_helpers.tpl
|
|
334
|
+
- deploy/helm/legion/templates/deployment-api.yaml
|
|
335
|
+
- deploy/helm/legion/templates/deployment-worker.yaml
|
|
336
|
+
- deploy/helm/legion/templates/hpa-worker.yaml
|
|
337
|
+
- deploy/helm/legion/templates/pdb.yaml
|
|
338
|
+
- deploy/helm/legion/templates/service-api.yaml
|
|
339
|
+
- deploy/helm/legion/templates/serviceaccount.yaml
|
|
340
|
+
- deploy/helm/legion/values.yaml
|
|
329
341
|
- docker_deploy.rb
|
|
330
342
|
- docs/README.md
|
|
331
343
|
- docs/plans/2026-03-18-config-import-vault-multicluster-design.md
|
|
@@ -418,6 +430,7 @@ files:
|
|
|
418
430
|
- lib/legion/cli/chat/web_fetch.rb
|
|
419
431
|
- lib/legion/cli/chat/web_search.rb
|
|
420
432
|
- lib/legion/cli/chat_command.rb
|
|
433
|
+
- lib/legion/cli/check/privacy_check.rb
|
|
421
434
|
- lib/legion/cli/check_command.rb
|
|
422
435
|
- lib/legion/cli/cohort.rb
|
|
423
436
|
- lib/legion/cli/coldstart_command.rb
|
|
@@ -446,6 +459,7 @@ files:
|
|
|
446
459
|
- lib/legion/cli/doctor/vault_check.rb
|
|
447
460
|
- lib/legion/cli/doctor_command.rb
|
|
448
461
|
- lib/legion/cli/error.rb
|
|
462
|
+
- lib/legion/cli/eval_command.rb
|
|
449
463
|
- lib/legion/cli/function.rb
|
|
450
464
|
- lib/legion/cli/gaia_command.rb
|
|
451
465
|
- lib/legion/cli/generate_command.rb
|