legionio 1.4.13 → 1.4.14
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/CHANGELOG.md +11 -0
- data/CLAUDE.md +22 -0
- data/Gemfile +1 -0
- data/README.md +40 -3
- data/lib/legion/api/rbac.rb +186 -0
- data/lib/legion/api.rb +4 -0
- data/lib/legion/cli/config_scaffold.rb +1 -1
- data/lib/legion/cli/connection.rb +1 -0
- data/lib/legion/cli/rbac_command.rb +209 -0
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/ingress.rb +8 -2
- data/lib/legion/mcp/server.rb +7 -1
- data/lib/legion/mcp/tools/rbac_assignments.rb +45 -0
- data/lib/legion/mcp/tools/rbac_check.rb +45 -0
- data/lib/legion/mcp/tools/rbac_grants.rb +41 -0
- data/lib/legion/service.rb +21 -0
- data/lib/legion/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0927bca374a6df75ab9a1e75067219af7205291a694defff2fa356fe2b330bf3'
|
|
4
|
+
data.tar.gz: 1c3171e36c3447a104889d556df53596e611b4965d60abb08d4e91fe5c50a996
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bba3556ee1380f6b61aaa14263dbcccb4340428237f0203cbed9d7090d1cf95e811109b75ebf14e1a1bd3767640ad805e2dee4b4d5711352aa7becd528bf259
|
|
7
|
+
data.tar.gz: 243d79f20efa83d65b41a6e39f3953aee765369f37aa30caa6b85d16cb0baeab95e46628a443c45b082f233925c3ba413734c5e8aa45146c3827e9f367597ae7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.14] - 2026-03-16
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Optional RBAC integration via legion-rbac gem (`if defined?(Legion::Rbac)` guards)
|
|
7
|
+
- `setup_rbac` lifecycle hook in Service (after setup_data)
|
|
8
|
+
- `authorize_execution!` guard in Ingress for task execution
|
|
9
|
+
- Rack middleware registration in API when legion-rbac loaded
|
|
10
|
+
- REST API routes for RBAC management (roles, assignments, grants, cross-team grants, check)
|
|
11
|
+
- `legion rbac` CLI subcommand (roles, show, assignments, assign, revoke, grants, grant, check)
|
|
12
|
+
- MCP tools: legion.rbac_check, legion.rbac_assignments, legion.rbac_grants
|
|
13
|
+
|
|
3
14
|
## [1.4.13] - 2026-03-16
|
|
4
15
|
|
|
5
16
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -346,6 +346,22 @@ legion
|
|
|
346
346
|
bash # output bash completion script
|
|
347
347
|
zsh # output zsh completion script
|
|
348
348
|
install # print installation instructions
|
|
349
|
+
|
|
350
|
+
openapi
|
|
351
|
+
generate [-o FILE] # output OpenAPI 3.1.0 spec JSON
|
|
352
|
+
routes # list all API routes with HTTP method + summary
|
|
353
|
+
|
|
354
|
+
doctor [--fix] [--json] # diagnose environment, suggest/apply fixes
|
|
355
|
+
# checks: Ruby, bundle, config, RabbitMQ, DB, cache, Vault,
|
|
356
|
+
# extensions, PID files, permissions
|
|
357
|
+
# exit 0=all pass, 1=any fail
|
|
358
|
+
|
|
359
|
+
telemetry
|
|
360
|
+
stats [SESSION_ID] # aggregate or per-session telemetry stats
|
|
361
|
+
ingest PATH # manually ingest a session log file
|
|
362
|
+
|
|
363
|
+
auth
|
|
364
|
+
teams [--tenant-id ID] [--client-id ID] # browser OAuth flow for Microsoft Teams
|
|
349
365
|
```
|
|
350
366
|
|
|
351
367
|
**CLI design rules:**
|
|
@@ -450,6 +466,8 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
|
|
|
450
466
|
| `lib/legion/api/coldstart.rb` | Coldstart: `POST /api/coldstart/ingest` — triggers lex-coldstart ingest runner (requires lex-coldstart + lex-memory) |
|
|
451
467
|
| `lib/legion/api/gaia.rb` | Gaia: system status endpoints |
|
|
452
468
|
| `lib/legion/api/token.rb` | Token: JWT token issuance endpoint |
|
|
469
|
+
| `lib/legion/api/openapi.rb` | OpenAPI: `Legion::API::OpenAPI.spec` / `.to_json`; also served at `GET /api/openapi.json` |
|
|
470
|
+
| `lib/legion/api/oauth.rb` | OAuth: `GET /api/oauth/microsoft_teams/callback` — receives delegated OAuth redirect and stores tokens |
|
|
453
471
|
| `lib/legion/api/middleware/auth.rb` | Auth: JWT Bearer auth middleware (real token validation, skip paths for health/ready) |
|
|
454
472
|
| **MCP** | |
|
|
455
473
|
| `lib/legion/mcp.rb` | Entry point: `Legion::MCP.server` singleton factory |
|
|
@@ -506,6 +524,10 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
|
|
|
506
524
|
| `lib/legion/cli/gaia_command.rb` | `legion gaia` subcommands (status) |
|
|
507
525
|
| `lib/legion/cli/schedule_command.rb` | `legion schedule` subcommands (list, show, add, remove, logs) |
|
|
508
526
|
| `lib/legion/cli/completion_command.rb` | `legion completion` subcommands (bash, zsh, install) |
|
|
527
|
+
| `lib/legion/cli/openapi_command.rb` | `legion openapi` subcommands (generate, routes); also `GET /api/openapi.json` endpoint |
|
|
528
|
+
| `lib/legion/cli/doctor_command.rb` | `legion doctor` — 10-check environment diagnosis; `Doctor::Result` value object with status/message/prescription/auto_fixable |
|
|
529
|
+
| `lib/legion/cli/telemetry_command.rb` | `legion telemetry` subcommands (stats, ingest) — session log analytics |
|
|
530
|
+
| `lib/legion/cli/auth_command.rb` | `legion auth` subcommands (teams) — delegated OAuth browser flow for external services |
|
|
509
531
|
| `completions/legion.bash` | Bash tab completion script |
|
|
510
532
|
| `completions/_legion` | Zsh tab completion script |
|
|
511
533
|
| `lib/legion/cli/theme.rb` | Purple palette, orbital ASCII banner, branded CLI output |
|
data/Gemfile
CHANGED
|
@@ -14,6 +14,7 @@ unless ENV['CI']
|
|
|
14
14
|
gem 'legion-json', path: '../legion-json'
|
|
15
15
|
gem 'legion-llm', path: '../legion-llm'
|
|
16
16
|
gem 'legion-logging', path: '../legion-logging'
|
|
17
|
+
gem 'legion-rbac', path: '../legion-rbac'
|
|
17
18
|
gem 'legion-settings', path: '../legion-settings'
|
|
18
19
|
gem 'legion-transport', path: '../legion-transport'
|
|
19
20
|
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Schedule tasks, chain services into dependency graphs, run them concurrently via
|
|
|
14
14
|
╰──────────────────────────────────────╯
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
**Ruby >= 3.4** | **v1.4.
|
|
17
|
+
**Ruby >= 3.4** | **v1.4.13** | **Apache-2.0** | [@Esity](https://github.com/Esity)
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -210,6 +210,16 @@ legion config scaffold # generate starter config files
|
|
|
210
210
|
|
|
211
211
|
Settings load from the first directory found: `/etc/legionio/` → `~/legionio/` → `./settings/`
|
|
212
212
|
|
|
213
|
+
### Diagnostics
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
legion doctor # diagnose environment, suggest fixes
|
|
217
|
+
legion doctor --fix # auto-remediate fixable issues (stale PIDs, missing gems)
|
|
218
|
+
legion doctor --json # machine-readable output
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Checks Ruby version, bundle status, config files, RabbitMQ, database, cache, Vault, extensions, PID files, and permissions. Exits 1 if any check fails.
|
|
222
|
+
|
|
213
223
|
All commands support `--json` for structured output and `--no-color` to strip ANSI codes.
|
|
214
224
|
|
|
215
225
|
## REST API
|
|
@@ -332,6 +342,25 @@ legion generate actor myactor
|
|
|
332
342
|
bundle exec rspec
|
|
333
343
|
```
|
|
334
344
|
|
|
345
|
+
## Role Profiles
|
|
346
|
+
|
|
347
|
+
Control which extensions load at startup via `settings/legion.json`:
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{"role": {"profile": "dev"}}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
| Profile | What loads |
|
|
354
|
+
|---------|-----------|
|
|
355
|
+
| *(default)* | Everything — no filtering |
|
|
356
|
+
| `core` | 14 core operational extensions only |
|
|
357
|
+
| `cognitive` | core + all agentic extensions |
|
|
358
|
+
| `service` | core + service + other integrations |
|
|
359
|
+
| `dev` | core + AI + essential agentic (~20 extensions) |
|
|
360
|
+
| `custom` | only what's listed in `role.extensions` |
|
|
361
|
+
|
|
362
|
+
Faster boot and lower memory footprint for dedicated worker roles.
|
|
363
|
+
|
|
335
364
|
## Scaling
|
|
336
365
|
|
|
337
366
|
Task distribution uses RabbitMQ FIFO queues. Add workers by running more Legion processes — each subscribes to the same queues and picks up work automatically. Tested to 100+ workers.
|
|
@@ -363,6 +392,12 @@ CMD ruby --yjit $(which legion) start
|
|
|
363
392
|
|
|
364
393
|
## Architecture
|
|
365
394
|
|
|
395
|
+
Before any Legion code loads, the executable applies three performance optimizations:
|
|
396
|
+
|
|
397
|
+
- **YJIT** — `RubyVM::YJIT.enable` for 15-30% runtime throughput (Ruby 3.1+ builds)
|
|
398
|
+
- **GC tuning** — pre-allocates 600k heap slots and raises malloc limits (ENV overrides respected)
|
|
399
|
+
- **bootsnap** — caches YARV bytecodes and `$LOAD_PATH` resolution at `~/.legionio/cache/bootsnap/`
|
|
400
|
+
|
|
366
401
|
```
|
|
367
402
|
legion start
|
|
368
403
|
└── Legion::Service
|
|
@@ -374,13 +409,15 @@ legion start
|
|
|
374
409
|
├── 6. Data (legion-data — database + migrations)
|
|
375
410
|
├── 7. LLM (legion-llm — AI provider setup + routing)
|
|
376
411
|
├── 8. Supervision (process supervision)
|
|
377
|
-
├── 9. Extensions (discover + load 280+ LEX gems)
|
|
412
|
+
├── 9. Extensions (discover + load 280+ LEX gems, filtered by role profile)
|
|
378
413
|
├── 10. Cluster Secret (distribute via Vault or memory)
|
|
379
414
|
└── 11. API (Sinatra/Puma on port 4567)
|
|
380
415
|
```
|
|
381
416
|
|
|
382
417
|
Each phase registers with `Legion::Readiness`. All phases are individually toggleable.
|
|
383
418
|
|
|
419
|
+
`SIGHUP` triggers a live reload (`Legion.reload`) — subsystems shut down in reverse order and restart fresh without killing the process. Useful for rolling restarts and config changes.
|
|
420
|
+
|
|
384
421
|
## Similar Projects
|
|
385
422
|
|
|
386
423
|
| Project | Language | HA | AI | Cognitive |
|
|
@@ -397,7 +434,7 @@ Each phase registers with `Legion::Readiness`. All phases are individually toggl
|
|
|
397
434
|
git clone https://github.com/LegionIO/LegionIO.git
|
|
398
435
|
cd LegionIO
|
|
399
436
|
bundle install
|
|
400
|
-
bundle exec rspec #
|
|
437
|
+
bundle exec rspec # 880 examples, 0 failures
|
|
401
438
|
bundle exec rubocop # 0 offenses
|
|
402
439
|
```
|
|
403
440
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
class API < Sinatra::Base
|
|
5
|
+
module Routes
|
|
6
|
+
module Rbac
|
|
7
|
+
def self.registered(app)
|
|
8
|
+
register_roles(app)
|
|
9
|
+
register_check(app)
|
|
10
|
+
register_assignments(app)
|
|
11
|
+
register_grants(app)
|
|
12
|
+
register_cross_team_grants(app)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.register_roles(app)
|
|
16
|
+
app.get '/api/rbac/roles' do
|
|
17
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
18
|
+
|
|
19
|
+
roles = Legion::Rbac.role_index.transform_values do |role|
|
|
20
|
+
{ name: role.name, description: role.description, cross_team: role.cross_team? }
|
|
21
|
+
end
|
|
22
|
+
json_response(roles)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
app.get '/api/rbac/roles/:name' do
|
|
26
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
27
|
+
|
|
28
|
+
role = Legion::Rbac.role_index[params[:name].to_sym]
|
|
29
|
+
halt 404, json_error('not_found', "Role #{params[:name]} not found", status_code: 404) unless role
|
|
30
|
+
|
|
31
|
+
json_response({
|
|
32
|
+
name: role.name,
|
|
33
|
+
description: role.description,
|
|
34
|
+
cross_team: role.cross_team?,
|
|
35
|
+
permissions: role.permissions.map { |p| { resource: p.resource_pattern, actions: p.actions } },
|
|
36
|
+
deny_rules: role.deny_rules.map { |d| { resource: d.resource_pattern, above_level: d.above_level } }
|
|
37
|
+
})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.register_check(app)
|
|
42
|
+
app.post '/api/rbac/check' do
|
|
43
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
44
|
+
|
|
45
|
+
body = parse_request_body
|
|
46
|
+
principal = Legion::Rbac::Principal.new(
|
|
47
|
+
id: body[:principal] || 'anonymous',
|
|
48
|
+
roles: body[:roles] || [],
|
|
49
|
+
team: body[:team]
|
|
50
|
+
)
|
|
51
|
+
result = Legion::Rbac::PolicyEngine.evaluate(
|
|
52
|
+
principal: principal,
|
|
53
|
+
action: body[:action] || 'read',
|
|
54
|
+
resource: body[:resource] || '*',
|
|
55
|
+
enforce: false
|
|
56
|
+
)
|
|
57
|
+
json_response(result)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.register_assignments(app)
|
|
62
|
+
app.get '/api/rbac/assignments' do
|
|
63
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
64
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
65
|
+
|
|
66
|
+
dataset = Legion::Data::Model::RbacRoleAssignment.order(:id)
|
|
67
|
+
dataset = dataset.where(team: params[:team]) if params[:team]
|
|
68
|
+
dataset = dataset.where(role: params[:role]) if params[:role]
|
|
69
|
+
dataset = dataset.where(principal_id: params[:principal]) if params[:principal]
|
|
70
|
+
json_collection(dataset)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
app.post '/api/rbac/assignments' do
|
|
74
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
75
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
76
|
+
|
|
77
|
+
body = parse_request_body
|
|
78
|
+
record = Legion::Data::Model::RbacRoleAssignment.create(
|
|
79
|
+
principal_type: body[:principal_type] || 'human',
|
|
80
|
+
principal_id: body[:principal_id],
|
|
81
|
+
role: body[:role],
|
|
82
|
+
team: body[:team],
|
|
83
|
+
granted_by: current_owner_msid || 'api',
|
|
84
|
+
expires_at: body[:expires_at] ? Time.parse(body[:expires_at]) : nil
|
|
85
|
+
)
|
|
86
|
+
json_response(record.values, status_code: 201)
|
|
87
|
+
rescue Sequel::ValidationFailed => e
|
|
88
|
+
json_error('validation_error', e.message, status_code: 422)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
app.delete '/api/rbac/assignments/:id' do
|
|
92
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
93
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
94
|
+
|
|
95
|
+
record = Legion::Data::Model::RbacRoleAssignment[params[:id].to_i]
|
|
96
|
+
halt 404, json_error('not_found', 'Assignment not found', status_code: 404) unless record
|
|
97
|
+
|
|
98
|
+
record.destroy
|
|
99
|
+
json_response({ deleted: true })
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.register_grants(app)
|
|
104
|
+
app.get '/api/rbac/grants' do
|
|
105
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
106
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
107
|
+
|
|
108
|
+
dataset = Legion::Data::Model::RbacRunnerGrant.order(:id)
|
|
109
|
+
dataset = dataset.where(team: params[:team]) if params[:team]
|
|
110
|
+
json_collection(dataset)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
app.post '/api/rbac/grants' do
|
|
114
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
115
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
116
|
+
|
|
117
|
+
body = parse_request_body
|
|
118
|
+
record = Legion::Data::Model::RbacRunnerGrant.create(
|
|
119
|
+
team: body[:team],
|
|
120
|
+
runner_pattern: body[:runner_pattern],
|
|
121
|
+
actions: Array(body[:actions]).join(','),
|
|
122
|
+
granted_by: current_owner_msid || 'api'
|
|
123
|
+
)
|
|
124
|
+
json_response(record.values, status_code: 201)
|
|
125
|
+
rescue Sequel::ValidationFailed => e
|
|
126
|
+
json_error('validation_error', e.message, status_code: 422)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
app.delete '/api/rbac/grants/:id' do
|
|
130
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
131
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
132
|
+
|
|
133
|
+
record = Legion::Data::Model::RbacRunnerGrant[params[:id].to_i]
|
|
134
|
+
halt 404, json_error('not_found', 'Grant not found', status_code: 404) unless record
|
|
135
|
+
|
|
136
|
+
record.destroy
|
|
137
|
+
json_response({ deleted: true })
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.register_cross_team_grants(app)
|
|
142
|
+
app.get '/api/rbac/grants/cross-team' do
|
|
143
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
144
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
145
|
+
|
|
146
|
+
dataset = Legion::Data::Model::RbacCrossTeamGrant.order(:id)
|
|
147
|
+
json_collection(dataset)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
app.post '/api/rbac/grants/cross-team' do
|
|
151
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
152
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
153
|
+
|
|
154
|
+
body = parse_request_body
|
|
155
|
+
record = Legion::Data::Model::RbacCrossTeamGrant.create(
|
|
156
|
+
source_team: body[:source_team],
|
|
157
|
+
target_team: body[:target_team],
|
|
158
|
+
runner_pattern: body[:runner_pattern],
|
|
159
|
+
actions: Array(body[:actions]).join(','),
|
|
160
|
+
granted_by: current_owner_msid || 'api',
|
|
161
|
+
expires_at: body[:expires_at] ? Time.parse(body[:expires_at]) : nil
|
|
162
|
+
)
|
|
163
|
+
json_response(record.values, status_code: 201)
|
|
164
|
+
rescue Sequel::ValidationFailed => e
|
|
165
|
+
json_error('validation_error', e.message, status_code: 422)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
app.delete '/api/rbac/grants/cross-team/:id' do
|
|
169
|
+
return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
|
|
170
|
+
return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
|
|
171
|
+
|
|
172
|
+
record = Legion::Data::Model::RbacCrossTeamGrant[params[:id].to_i]
|
|
173
|
+
halt 404, json_error('not_found', 'Grant not found', status_code: 404) unless record
|
|
174
|
+
|
|
175
|
+
record.destroy
|
|
176
|
+
json_response({ deleted: true })
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class << self
|
|
181
|
+
private :register_roles, :register_check, :register_assignments, :register_grants, :register_cross_team_grants
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
data/lib/legion/api.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative 'api/coldstart'
|
|
|
22
22
|
require_relative 'api/gaia'
|
|
23
23
|
require_relative 'api/oauth'
|
|
24
24
|
require_relative 'api/openapi'
|
|
25
|
+
require_relative 'api/rbac'
|
|
25
26
|
|
|
26
27
|
module Legion
|
|
27
28
|
class API < Sinatra::Base
|
|
@@ -88,6 +89,9 @@ module Legion
|
|
|
88
89
|
register Routes::Coldstart
|
|
89
90
|
register Routes::Gaia
|
|
90
91
|
register Routes::OAuth
|
|
92
|
+
register Routes::Rbac
|
|
93
|
+
|
|
94
|
+
use Legion::Rbac::Middleware if defined?(Legion::Rbac::Middleware)
|
|
91
95
|
|
|
92
96
|
# Hook registry (preserved from original implementation)
|
|
93
97
|
class << self
|
|
@@ -11,7 +11,7 @@ module Legion
|
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
13
|
def run(formatter, options)
|
|
14
|
-
dir = options[:dir] ||
|
|
14
|
+
dir = options[:dir] || "#{Dir.home}/.legionio/settings"
|
|
15
15
|
only = options[:only] ? options[:only].split(',').map(&:strip) : SUBSYSTEMS
|
|
16
16
|
full_mode = options[:full]
|
|
17
17
|
force = options[:force]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module CLI
|
|
5
|
+
class Rbac < Thor
|
|
6
|
+
def self.exit_on_failure?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
11
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
12
|
+
class_option :verbose, type: :boolean, default: false, aliases: ['-V'], desc: 'Verbose logging'
|
|
13
|
+
class_option :config_dir, type: :string, desc: 'Config directory path'
|
|
14
|
+
|
|
15
|
+
desc 'roles', 'List role definitions from config'
|
|
16
|
+
def roles
|
|
17
|
+
out = formatter
|
|
18
|
+
with_rbac do
|
|
19
|
+
index = Legion::Rbac.role_index
|
|
20
|
+
if options[:json]
|
|
21
|
+
out.json(index.transform_values { |r| { description: r.description, cross_team: r.cross_team? } })
|
|
22
|
+
else
|
|
23
|
+
rows = index.map { |name, r| [name.to_s, r.description, r.cross_team? ? 'yes' : 'no'] }
|
|
24
|
+
out.table(%w[Role Description CrossTeam], rows)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
default_task :roles
|
|
29
|
+
|
|
30
|
+
desc 'show ROLE', 'Show permissions for a role'
|
|
31
|
+
def show(role_name)
|
|
32
|
+
out = formatter
|
|
33
|
+
with_rbac do
|
|
34
|
+
role = Legion::Rbac.role_index[role_name.to_sym]
|
|
35
|
+
unless role
|
|
36
|
+
out.error("Role not found: #{role_name}")
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if options[:json]
|
|
41
|
+
out.json({
|
|
42
|
+
name: role.name,
|
|
43
|
+
description: role.description,
|
|
44
|
+
cross_team: role.cross_team?,
|
|
45
|
+
permissions: role.permissions.map { |p| { resource: p.resource_pattern, actions: p.actions } },
|
|
46
|
+
deny_rules: role.deny_rules.map { |d| { resource: d.resource_pattern, above_level: d.above_level } }
|
|
47
|
+
})
|
|
48
|
+
else
|
|
49
|
+
out.header("Role: #{role.name}")
|
|
50
|
+
puts " #{role.description}"
|
|
51
|
+
puts " Cross-team: #{role.cross_team? ? 'yes' : 'no'}"
|
|
52
|
+
puts "\n Permissions:"
|
|
53
|
+
role.permissions.each { |p| puts " #{p.resource_pattern} -> #{p.actions.join(', ')}" }
|
|
54
|
+
puts "\n Deny rules:"
|
|
55
|
+
role.deny_rules.each { |d| puts " #{d.resource_pattern}#{" (above level #{d.above_level})" if d.above_level}" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc 'assignments', 'List role assignments from DB'
|
|
61
|
+
option :team, type: :string, desc: 'Filter by team'
|
|
62
|
+
option :role, type: :string, desc: 'Filter by role'
|
|
63
|
+
option :principal, type: :string, desc: 'Filter by principal ID'
|
|
64
|
+
def assignments
|
|
65
|
+
out = formatter
|
|
66
|
+
with_data do
|
|
67
|
+
ds = Legion::Data::Model::RbacRoleAssignment.dataset
|
|
68
|
+
ds = ds.where(team: options[:team]) if options[:team]
|
|
69
|
+
ds = ds.where(role: options[:role]) if options[:role]
|
|
70
|
+
ds = ds.where(principal_id: options[:principal]) if options[:principal]
|
|
71
|
+
|
|
72
|
+
records = ds.all
|
|
73
|
+
if options[:json]
|
|
74
|
+
out.json(records.map(&:values))
|
|
75
|
+
else
|
|
76
|
+
rows = records.map { |r| [r.id, r.principal_id, r.principal_type, r.role, r.team || '-', r.active? ? 'active' : 'expired'] }
|
|
77
|
+
out.table(%w[ID Principal Type Role Team Status], rows)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc 'assign PRINCIPAL ROLE', 'Assign a role to a principal'
|
|
83
|
+
option :type, type: :string, default: 'human', desc: 'Principal type (human/worker)'
|
|
84
|
+
option :team, type: :string, desc: 'Team scope'
|
|
85
|
+
option :expires, type: :string, desc: 'Expiry (ISO 8601)'
|
|
86
|
+
def assign(principal, role)
|
|
87
|
+
out = formatter
|
|
88
|
+
with_data do
|
|
89
|
+
record = Legion::Data::Model::RbacRoleAssignment.create(
|
|
90
|
+
principal_type: options[:type],
|
|
91
|
+
principal_id: principal,
|
|
92
|
+
role: role,
|
|
93
|
+
team: options[:team],
|
|
94
|
+
granted_by: 'cli',
|
|
95
|
+
expires_at: options[:expires] ? Time.parse(options[:expires]) : nil
|
|
96
|
+
)
|
|
97
|
+
out.success("Assigned #{role} to #{principal} (id: #{record.id})")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
desc 'revoke PRINCIPAL ROLE', 'Remove a role assignment'
|
|
102
|
+
def revoke(principal, role)
|
|
103
|
+
out = formatter
|
|
104
|
+
with_data do
|
|
105
|
+
ds = Legion::Data::Model::RbacRoleAssignment.where(principal_id: principal, role: role)
|
|
106
|
+
count = ds.count
|
|
107
|
+
ds.destroy
|
|
108
|
+
out.success("Revoked #{count} assignment(s) of #{role} from #{principal}")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc 'grants', 'List runner grants'
|
|
113
|
+
option :team, type: :string, desc: 'Filter by team'
|
|
114
|
+
def grants
|
|
115
|
+
out = formatter
|
|
116
|
+
with_data do
|
|
117
|
+
ds = Legion::Data::Model::RbacRunnerGrant.dataset
|
|
118
|
+
ds = ds.where(team: options[:team]) if options[:team]
|
|
119
|
+
|
|
120
|
+
records = ds.all
|
|
121
|
+
if options[:json]
|
|
122
|
+
out.json(records.map(&:values))
|
|
123
|
+
else
|
|
124
|
+
rows = records.map { |r| [r.id, r.team, r.runner_pattern, r.actions] }
|
|
125
|
+
out.table(%w[ID Team Pattern Actions], rows)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
desc 'grant TEAM PATTERN', 'Grant runner access to a team'
|
|
131
|
+
option :actions, type: :string, default: 'execute', desc: 'Comma-separated actions'
|
|
132
|
+
def grant(team, pattern)
|
|
133
|
+
out = formatter
|
|
134
|
+
with_data do
|
|
135
|
+
record = Legion::Data::Model::RbacRunnerGrant.create(
|
|
136
|
+
team: team,
|
|
137
|
+
runner_pattern: pattern,
|
|
138
|
+
actions: options[:actions],
|
|
139
|
+
granted_by: 'cli'
|
|
140
|
+
)
|
|
141
|
+
out.success("Granted #{pattern} to team #{team} (id: #{record.id})")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
desc 'check PRINCIPAL RESOURCE', 'Dry-run authorization check'
|
|
146
|
+
option :action, type: :string, default: 'read', desc: 'Action to check'
|
|
147
|
+
option :roles, type: :array, default: [], desc: 'Roles to check (comma-separated)'
|
|
148
|
+
option :team, type: :string, desc: 'Team scope'
|
|
149
|
+
def check(principal_id, resource)
|
|
150
|
+
out = formatter
|
|
151
|
+
with_rbac do
|
|
152
|
+
principal = Legion::Rbac::Principal.new(
|
|
153
|
+
id: principal_id,
|
|
154
|
+
roles: options[:roles],
|
|
155
|
+
team: options[:team]
|
|
156
|
+
)
|
|
157
|
+
result = Legion::Rbac::PolicyEngine.evaluate(
|
|
158
|
+
principal: principal,
|
|
159
|
+
action: options[:action],
|
|
160
|
+
resource: resource,
|
|
161
|
+
enforce: false
|
|
162
|
+
)
|
|
163
|
+
if options[:json]
|
|
164
|
+
out.json(result)
|
|
165
|
+
else
|
|
166
|
+
status = result[:allowed] ? 'ALLOWED' : 'DENIED'
|
|
167
|
+
puts " #{status}: #{principal_id} -> #{options[:action]} #{resource}"
|
|
168
|
+
puts " Reason: #{result[:reason]}" if result[:reason]
|
|
169
|
+
puts " Would deny: #{result[:would_deny]}" if result[:would_deny]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
no_commands do
|
|
175
|
+
def formatter
|
|
176
|
+
@formatter ||= Output::Formatter.new(json: options[:json], color: !options[:no_color])
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def with_rbac
|
|
182
|
+
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
183
|
+
Connection.log_level = options[:verbose] ? 'debug' : 'error'
|
|
184
|
+
Connection.ensure_settings
|
|
185
|
+
require 'legion/rbac'
|
|
186
|
+
Legion::Rbac.setup
|
|
187
|
+
yield
|
|
188
|
+
rescue CLI::Error => e
|
|
189
|
+
formatter.error(e.message)
|
|
190
|
+
raise SystemExit, 1
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def with_data
|
|
194
|
+
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
195
|
+
Connection.log_level = options[:verbose] ? 'debug' : 'error'
|
|
196
|
+
Connection.ensure_data
|
|
197
|
+
require 'legion/rbac'
|
|
198
|
+
Legion::Rbac.setup
|
|
199
|
+
yield
|
|
200
|
+
rescue CLI::Error => e
|
|
201
|
+
formatter.error(e.message)
|
|
202
|
+
raise SystemExit, 1
|
|
203
|
+
ensure
|
|
204
|
+
Connection.shutdown
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Legion
|
|
|
33
33
|
autoload :Doctor, 'legion/cli/doctor_command'
|
|
34
34
|
autoload :Telemetry, 'legion/cli/telemetry_command'
|
|
35
35
|
autoload :Auth, 'legion/cli/auth_command'
|
|
36
|
+
autoload :Rbac, 'legion/cli/rbac_command'
|
|
36
37
|
|
|
37
38
|
class Main < Thor
|
|
38
39
|
def self.exit_on_failure?
|
|
@@ -195,6 +196,9 @@ module Legion
|
|
|
195
196
|
desc 'auth SUBCOMMAND', 'Authenticate with external services'
|
|
196
197
|
subcommand 'auth', Legion::CLI::Auth
|
|
197
198
|
|
|
199
|
+
desc 'rbac SUBCOMMAND', 'Role-based access control management'
|
|
200
|
+
subcommand 'rbac', Legion::CLI::Rbac
|
|
201
|
+
|
|
198
202
|
desc 'tree', 'Print a tree of all available commands'
|
|
199
203
|
def tree
|
|
200
204
|
legion_print_command_tree(self.class, 'legion', '')
|
data/lib/legion/ingress.rb
CHANGED
|
@@ -25,11 +25,12 @@ module Legion
|
|
|
25
25
|
|
|
26
26
|
# Normalize and execute via Legion::Runner.run.
|
|
27
27
|
# Returns the runner result hash.
|
|
28
|
-
def run(payload:, runner_class: nil, function: nil, source: 'unknown', **opts)
|
|
28
|
+
def run(payload:, runner_class: nil, function: nil, source: 'unknown', principal: nil, **opts) # rubocop:disable Metrics/ParameterLists
|
|
29
29
|
check_subtask = opts.fetch(:check_subtask, true)
|
|
30
30
|
generate_task = opts.fetch(:generate_task, true)
|
|
31
31
|
message = normalize(payload: payload, runner_class: runner_class,
|
|
32
|
-
function: function, source: source,
|
|
32
|
+
function: function, source: source,
|
|
33
|
+
**opts.except(:check_subtask, :generate_task, :principal))
|
|
33
34
|
|
|
34
35
|
rc = message.delete(:runner_class)
|
|
35
36
|
fn = message.delete(:function)
|
|
@@ -37,6 +38,11 @@ module Legion
|
|
|
37
38
|
raise 'runner_class is required' if rc.nil?
|
|
38
39
|
raise 'function is required' if fn.nil?
|
|
39
40
|
|
|
41
|
+
if defined?(Legion::Rbac)
|
|
42
|
+
principal ||= Legion::Rbac::Principal.local_admin
|
|
43
|
+
Legion::Rbac.authorize_execution!(principal: principal, runner_class: rc.to_s, function: fn.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
Legion::Events.emit('ingress.received', runner_class: rc.to_s, function: fn, source: source)
|
|
41
47
|
|
|
42
48
|
Legion::Runner.run(
|
data/lib/legion/mcp/server.rb
CHANGED
|
@@ -30,6 +30,9 @@ require_relative 'tools/worker_lifecycle'
|
|
|
30
30
|
require_relative 'tools/worker_costs'
|
|
31
31
|
require_relative 'tools/team_summary'
|
|
32
32
|
require_relative 'tools/routing_stats'
|
|
33
|
+
require_relative 'tools/rbac_check'
|
|
34
|
+
require_relative 'tools/rbac_assignments'
|
|
35
|
+
require_relative 'tools/rbac_grants'
|
|
33
36
|
require_relative 'resources/runner_catalog'
|
|
34
37
|
require_relative 'resources/extension_info'
|
|
35
38
|
|
|
@@ -66,7 +69,10 @@ module Legion
|
|
|
66
69
|
Tools::WorkerLifecycle,
|
|
67
70
|
Tools::WorkerCosts,
|
|
68
71
|
Tools::TeamSummary,
|
|
69
|
-
Tools::RoutingStats
|
|
72
|
+
Tools::RoutingStats,
|
|
73
|
+
Tools::RbacCheck,
|
|
74
|
+
Tools::RbacAssignments,
|
|
75
|
+
Tools::RbacGrants
|
|
70
76
|
].freeze
|
|
71
77
|
|
|
72
78
|
class << self
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RbacAssignments < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.rbac_assignments'
|
|
8
|
+
description 'List RBAC role assignments. Filterable by team, role, or principal.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
team: { type: 'string', description: 'Filter by team' },
|
|
13
|
+
role: { type: 'string', description: 'Filter by role name' },
|
|
14
|
+
principal: { type: 'string', description: 'Filter by principal ID' }
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(team: nil, role: nil, principal: nil)
|
|
20
|
+
return error_response('legion-rbac not installed') unless defined?(Legion::Rbac)
|
|
21
|
+
return error_response('legion-data not connected') unless Legion::Rbac::Store.db_available?
|
|
22
|
+
|
|
23
|
+
ds = Legion::Data::Model::RbacRoleAssignment.dataset
|
|
24
|
+
ds = ds.where(team: team) if team
|
|
25
|
+
ds = ds.where(role: role) if role
|
|
26
|
+
ds = ds.where(principal_id: principal) if principal
|
|
27
|
+
text_response(ds.all.map(&:values))
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
error_response("Failed to list assignments: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def text_response(data)
|
|
35
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error_response(msg)
|
|
39
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RbacCheck < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.rbac_check'
|
|
8
|
+
description 'Dry-run authorization check. Evaluates RBAC policies without enforcing.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
principal: { type: 'string', description: 'Principal ID to check' },
|
|
13
|
+
action: { type: 'string', description: 'Action (read, execute, manage, etc.)' },
|
|
14
|
+
resource: { type: 'string', description: 'Resource path (e.g. runners/lex-github/*)' },
|
|
15
|
+
roles: { type: 'array', items: { type: 'string' }, description: 'Roles to evaluate' },
|
|
16
|
+
team: { type: 'string', description: 'Team scope' }
|
|
17
|
+
},
|
|
18
|
+
required: %w[principal action resource roles]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def call(principal:, action:, resource:, roles: [], team: nil)
|
|
23
|
+
return error_response('legion-rbac not installed') unless defined?(Legion::Rbac)
|
|
24
|
+
|
|
25
|
+
p = Legion::Rbac::Principal.new(id: principal, roles: roles, team: team)
|
|
26
|
+
result = Legion::Rbac::PolicyEngine.evaluate(principal: p, action: action, resource: resource, enforce: false)
|
|
27
|
+
text_response(result)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
error_response("RBAC check failed: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def text_response(data)
|
|
35
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error_response(msg)
|
|
39
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RbacGrants < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.rbac_grants'
|
|
8
|
+
description 'List RBAC runner grants. Filterable by team.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
team: { type: 'string', description: 'Filter by team' }
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def call(team: nil)
|
|
18
|
+
return error_response('legion-rbac not installed') unless defined?(Legion::Rbac)
|
|
19
|
+
return error_response('legion-data not connected') unless Legion::Rbac::Store.db_available?
|
|
20
|
+
|
|
21
|
+
ds = Legion::Data::Model::RbacRunnerGrant.dataset
|
|
22
|
+
ds = ds.where(team: team) if team
|
|
23
|
+
text_response(ds.all.map(&:values))
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
error_response("Failed to list grants: #{e.message}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def text_response(data)
|
|
31
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def error_response(msg)
|
|
35
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/legion/service.rb
CHANGED
|
@@ -25,6 +25,8 @@ module Legion
|
|
|
25
25
|
Legion::Readiness.mark_ready(:crypt)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
Legion::Settings.resolve_secrets!
|
|
29
|
+
|
|
28
30
|
if transport
|
|
29
31
|
setup_transport
|
|
30
32
|
Legion::Readiness.mark_ready(:transport)
|
|
@@ -41,6 +43,8 @@ module Legion
|
|
|
41
43
|
Legion::Readiness.mark_ready(:data)
|
|
42
44
|
end
|
|
43
45
|
|
|
46
|
+
setup_rbac if data
|
|
47
|
+
|
|
44
48
|
if llm
|
|
45
49
|
setup_llm
|
|
46
50
|
Legion::Readiness.mark_ready(:llm)
|
|
@@ -74,10 +78,22 @@ module Legion
|
|
|
74
78
|
Legion::Logging.warn "Legion::Data failed to load, starting without it. e: #{e.message}"
|
|
75
79
|
end
|
|
76
80
|
|
|
81
|
+
def setup_rbac
|
|
82
|
+
require 'legion/rbac'
|
|
83
|
+
Legion::Rbac.setup
|
|
84
|
+
Legion::Readiness.mark_ready(:rbac)
|
|
85
|
+
Legion::Logging.info 'Legion::Rbac loaded'
|
|
86
|
+
rescue LoadError
|
|
87
|
+
Legion::Logging.debug 'Legion::Rbac gem is not installed, starting without RBAC'
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
Legion::Logging.warn "Legion::Rbac failed to load: #{e.message}"
|
|
90
|
+
end
|
|
91
|
+
|
|
77
92
|
# noinspection RubyArgCount
|
|
78
93
|
def default_paths
|
|
79
94
|
[
|
|
80
95
|
'/etc/legionio',
|
|
96
|
+
"#{Dir.home}/.legionio/settings",
|
|
81
97
|
"#{ENV.fetch('home', nil)}/legionio",
|
|
82
98
|
'~/legionio',
|
|
83
99
|
'./settings'
|
|
@@ -212,6 +228,11 @@ module Legion
|
|
|
212
228
|
Legion::Readiness.mark_not_ready(:llm)
|
|
213
229
|
end
|
|
214
230
|
|
|
231
|
+
if defined?(Legion::Rbac) && Legion::Settings[:rbac]&.dig(:connected)
|
|
232
|
+
Legion::Rbac.shutdown
|
|
233
|
+
Legion::Readiness.mark_not_ready(:rbac)
|
|
234
|
+
end
|
|
235
|
+
|
|
215
236
|
Legion::Data.shutdown if Legion::Settings[:data][:connected]
|
|
216
237
|
Legion::Readiness.mark_not_ready(:data)
|
|
217
238
|
|
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.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -326,6 +326,7 @@ files:
|
|
|
326
326
|
- lib/legion/api/nodes.rb
|
|
327
327
|
- lib/legion/api/oauth.rb
|
|
328
328
|
- lib/legion/api/openapi.rb
|
|
329
|
+
- lib/legion/api/rbac.rb
|
|
329
330
|
- lib/legion/api/relationships.rb
|
|
330
331
|
- lib/legion/api/schedules.rb
|
|
331
332
|
- lib/legion/api/settings.rb
|
|
@@ -426,6 +427,7 @@ files:
|
|
|
426
427
|
- lib/legion/cli/output.rb
|
|
427
428
|
- lib/legion/cli/plan_command.rb
|
|
428
429
|
- lib/legion/cli/pr_command.rb
|
|
430
|
+
- lib/legion/cli/rbac_command.rb
|
|
429
431
|
- lib/legion/cli/relationship.rb
|
|
430
432
|
- lib/legion/cli/review_command.rb
|
|
431
433
|
- lib/legion/cli/schedule_command.rb
|
|
@@ -500,6 +502,9 @@ files:
|
|
|
500
502
|
- lib/legion/mcp/tools/list_schedules.rb
|
|
501
503
|
- lib/legion/mcp/tools/list_tasks.rb
|
|
502
504
|
- lib/legion/mcp/tools/list_workers.rb
|
|
505
|
+
- lib/legion/mcp/tools/rbac_assignments.rb
|
|
506
|
+
- lib/legion/mcp/tools/rbac_check.rb
|
|
507
|
+
- lib/legion/mcp/tools/rbac_grants.rb
|
|
503
508
|
- lib/legion/mcp/tools/routing_stats.rb
|
|
504
509
|
- lib/legion/mcp/tools/run_task.rb
|
|
505
510
|
- lib/legion/mcp/tools/show_worker.rb
|