lex-uais 0.1.0
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 +7 -0
- data/.github/CODEOWNERS +7 -0
- data/.github/workflows/ci.yml +34 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +50 -0
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +75 -0
- data/Gemfile +17 -0
- data/README.md +69 -0
- data/lex-uais.gemspec +28 -0
- data/lib/legion/extensions/uais/client.rb +23 -0
- data/lib/legion/extensions/uais/helpers/client.rb +46 -0
- data/lib/legion/extensions/uais/runners/registration.rb +118 -0
- data/lib/legion/extensions/uais/version.rb +9 -0
- data/lib/legion/extensions/uais.rb +24 -0
- data/spec/legion/extensions/uais/client_spec.rb +45 -0
- data/spec/legion/extensions/uais/helpers/client_spec.rb +63 -0
- data/spec/legion/extensions/uais/runners/registration_spec.rb +111 -0
- data/spec/legion/extensions/uais_spec.rb +32 -0
- data/spec/spec_helper.rb +50 -0
- metadata +106 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b6c9562fc2b37ffa26eec00a63d7c0bd0bccb34feb807ba5f7f9e5a1d36d253f
|
|
4
|
+
data.tar.gz: 4b7a16c4dc99c84dd2766b770ce6f569ab65f2c46a442ae8b18dc3407ef08d51
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 465dc286dcfcfa2471447df60517e57c727cc7e53a966b16ac69ed5c1766448cf1f5e871d72e9d9dace0c7818b721d4d4a8a1b87a0fa223e8d3347c3d632ed64
|
|
7
|
+
data.tar.gz: 3ad6cb3bf597404a8d5931e34b024661883236b1567c68605b0e708363dbd1ddbd0848c1125acb18e82a345c89a103b4718aa86c3b307289a12c449fc3eab565
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
schedule:
|
|
7
|
+
- cron: '0 9 * * 1'
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci:
|
|
11
|
+
uses: LegionIO/.github/.github/workflows/ci.yml@main
|
|
12
|
+
|
|
13
|
+
lint:
|
|
14
|
+
uses: LegionIO/.github/.github/workflows/lint-patterns.yml@main
|
|
15
|
+
|
|
16
|
+
security:
|
|
17
|
+
uses: LegionIO/.github/.github/workflows/security-scan.yml@main
|
|
18
|
+
|
|
19
|
+
version-changelog:
|
|
20
|
+
uses: LegionIO/.github/.github/workflows/version-changelog.yml@main
|
|
21
|
+
|
|
22
|
+
dependency-review:
|
|
23
|
+
uses: LegionIO/.github/.github/workflows/dependency-review.yml@main
|
|
24
|
+
|
|
25
|
+
stale:
|
|
26
|
+
if: github.event_name == 'schedule'
|
|
27
|
+
uses: LegionIO/.github/.github/workflows/stale.yml@main
|
|
28
|
+
|
|
29
|
+
release:
|
|
30
|
+
needs: [ci, lint]
|
|
31
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
32
|
+
uses: LegionIO/.github/.github/workflows/release.yml@main
|
|
33
|
+
secrets:
|
|
34
|
+
rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.4
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
6
|
+
Layout/LineLength:
|
|
7
|
+
Max: 160
|
|
8
|
+
|
|
9
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
|
10
|
+
EnforcedStyle: space
|
|
11
|
+
|
|
12
|
+
Layout/HashAlignment:
|
|
13
|
+
EnforcedHashRocketStyle: table
|
|
14
|
+
EnforcedColonStyle: table
|
|
15
|
+
|
|
16
|
+
Metrics/MethodLength:
|
|
17
|
+
Max: 50
|
|
18
|
+
|
|
19
|
+
Metrics/ClassLength:
|
|
20
|
+
Max: 1500
|
|
21
|
+
|
|
22
|
+
Metrics/ModuleLength:
|
|
23
|
+
Max: 1500
|
|
24
|
+
|
|
25
|
+
Metrics/BlockLength:
|
|
26
|
+
Max: 40
|
|
27
|
+
Exclude:
|
|
28
|
+
- 'spec/**/*'
|
|
29
|
+
|
|
30
|
+
Metrics/AbcSize:
|
|
31
|
+
Max: 60
|
|
32
|
+
|
|
33
|
+
Metrics/CyclomaticComplexity:
|
|
34
|
+
Max: 15
|
|
35
|
+
|
|
36
|
+
Metrics/PerceivedComplexity:
|
|
37
|
+
Max: 17
|
|
38
|
+
|
|
39
|
+
Style/Documentation:
|
|
40
|
+
Enabled: false
|
|
41
|
+
|
|
42
|
+
Style/SymbolArray:
|
|
43
|
+
Enabled: true
|
|
44
|
+
|
|
45
|
+
Style/FrozenStringLiteralComment:
|
|
46
|
+
Enabled: true
|
|
47
|
+
EnforcedStyle: always
|
|
48
|
+
|
|
49
|
+
Naming/FileName:
|
|
50
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-03-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release
|
|
7
|
+
- UAIS agent registration client (register, deregister, check, AIRB status)
|
|
8
|
+
- Mock adapter for development/testing (default when no base_url configured)
|
|
9
|
+
- Soft-warn enforcement (registration failures log warnings, never block)
|
|
10
|
+
- Net::HTTP-based connection helper
|
|
11
|
+
- Standalone Client class for use outside Legion framework
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# lex-uais: UAIS Registration Extension for LegionIO
|
|
2
|
+
|
|
3
|
+
**Repository Level 3 Documentation**
|
|
4
|
+
- **Parent (Level 2)**: `/Users/miverso2/rubymine/legion/extensions/CLAUDE.md`
|
|
5
|
+
- **Parent (Level 1)**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
Legion Extension that provides a client for United AI Studio (UAIS) agent registration. Registers, deregisters, and queries Digital Worker status with the UAIS platform. Defaults to mock mode when no `base_url` is configured, and uses soft-warn enforcement so registration failures never block operations.
|
|
10
|
+
|
|
11
|
+
**Version**: 0.1.0
|
|
12
|
+
**GitHub**: https://github.com/LegionIO/lex-uais
|
|
13
|
+
**License**: Apache-2.0
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Legion::Extensions::Uais
|
|
19
|
+
├── Runners/
|
|
20
|
+
│ └── Registration # register_worker, deregister_worker, check_registration, airb_status
|
|
21
|
+
├── Helpers/
|
|
22
|
+
│ └── Client # Net::HTTP connection builder with build_request helper
|
|
23
|
+
└── Client # Standalone client class (includes all runners)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Key Files
|
|
27
|
+
|
|
28
|
+
| Path | Purpose |
|
|
29
|
+
|------|---------|
|
|
30
|
+
| `lib/legion/extensions/uais.rb` | Entry point, extension registration, `default_settings` |
|
|
31
|
+
| `lib/legion/extensions/uais/runners/registration.rb` | Registration logic (all 4 operations) + mock adapter + soft-warn |
|
|
32
|
+
| `lib/legion/extensions/uais/helpers/client.rb` | Net::HTTP connection builder with timeout and auth |
|
|
33
|
+
| `lib/legion/extensions/uais/client.rb` | Standalone `Client` class for use outside Legion framework |
|
|
34
|
+
|
|
35
|
+
## Runner Methods
|
|
36
|
+
|
|
37
|
+
All methods are in `Runners::Registration` and return hashes with status info. On failure, they log warnings via `soft_warn` and return error hashes instead of raising.
|
|
38
|
+
|
|
39
|
+
| Method | Args | Notes |
|
|
40
|
+
|--------|------|-------|
|
|
41
|
+
| `register_worker` | `worker_id:, name:, owner_msid:, extension_name:, **opts` | opts: `risk_tier:`, `team:`, `business_role:` |
|
|
42
|
+
| `deregister_worker` | `worker_id:, reason: nil` | |
|
|
43
|
+
| `check_registration` | `worker_id:` | |
|
|
44
|
+
| `airb_status` | `worker_id:` | AIRB approval status |
|
|
45
|
+
|
|
46
|
+
## Mock Mode
|
|
47
|
+
|
|
48
|
+
Mock mode is active when `mock: true` (default) or `base_url` is nil. All operations return success with `mock: true` in the response hash.
|
|
49
|
+
|
|
50
|
+
## Notes
|
|
51
|
+
|
|
52
|
+
- `default_settings` defines `mock: true`, `base_url: nil`, `timeout: 30`
|
|
53
|
+
- Uses Net::HTTP directly (no Faraday dependency)
|
|
54
|
+
- `soft_warn` logs via `Legion::Logging.warn` if available; never raises
|
|
55
|
+
- Standalone `Client` class auto-enables mock when `base_url` is nil
|
|
56
|
+
|
|
57
|
+
## Dependencies
|
|
58
|
+
|
|
59
|
+
| Gem | Purpose |
|
|
60
|
+
|-----|---------|
|
|
61
|
+
| `legion-json` | JSON serialization |
|
|
62
|
+
| `legion-logging` | Logging (optional, for soft-warn) |
|
|
63
|
+
| `legion-settings` | Configuration management |
|
|
64
|
+
|
|
65
|
+
## Testing
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bundle install
|
|
69
|
+
bundle exec rspec
|
|
70
|
+
bundle exec rubocop
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
**Maintained By**: Matthew Iverson (@Esity)
|
data/Gemfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
group :test do
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rspec'
|
|
9
|
+
gem 'rspec_junit_formatter'
|
|
10
|
+
gem 'rubocop'
|
|
11
|
+
gem 'simplecov'
|
|
12
|
+
|
|
13
|
+
gem 'legion-cache'
|
|
14
|
+
gem 'legion-crypt'
|
|
15
|
+
gem 'legion-data'
|
|
16
|
+
gem 'legion-transport'
|
|
17
|
+
end
|
data/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# lex-uais
|
|
2
|
+
|
|
3
|
+
LegionIO extension for United AI Studio (UAIS) agent registration. Provides a client for registering, deregistering, and checking Digital Worker registration status with the UAIS platform.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Register/deregister Digital Workers with UAIS
|
|
8
|
+
- Check registration status and AIRB approval
|
|
9
|
+
- Mock adapter (default) for development and testing
|
|
10
|
+
- Soft-warn enforcement: failures log warnings but never block operations
|
|
11
|
+
- Standalone `Client` class for use outside the Legion framework
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem 'lex-uais'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Standalone Client
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require 'legion/extensions/uais/client'
|
|
27
|
+
|
|
28
|
+
# Mock mode (default when no base_url)
|
|
29
|
+
client = Legion::Extensions::Uais::Client.new
|
|
30
|
+
result = client.register_worker(worker_id: 'w-123', name: 'InvoiceBot', owner_msid: 'ms123', extension_name: 'lex-invoice')
|
|
31
|
+
|
|
32
|
+
# Live mode
|
|
33
|
+
client = Legion::Extensions::Uais::Client.new(base_url: 'https://api.uais.uhg.com', api_key: 'key')
|
|
34
|
+
result = client.register_worker(worker_id: 'w-123', name: 'InvoiceBot', owner_msid: 'ms123', extension_name: 'lex-invoice')
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Within Legion Framework
|
|
38
|
+
|
|
39
|
+
The extension is auto-discovered via the `lex-*` naming convention. Configure via settings:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
uais:
|
|
43
|
+
options:
|
|
44
|
+
base_url: https://api.uais.uhg.com
|
|
45
|
+
api_key: <vault-sourced>
|
|
46
|
+
mock: false
|
|
47
|
+
timeout: 30
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Runner Methods
|
|
51
|
+
|
|
52
|
+
| Method | Description |
|
|
53
|
+
|--------|-------------|
|
|
54
|
+
| `register_worker` | Register a Digital Worker with UAIS |
|
|
55
|
+
| `deregister_worker` | Deregister a worker |
|
|
56
|
+
| `check_registration` | Check if a worker is registered |
|
|
57
|
+
| `airb_status` | Check AIRB approval status |
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bundle install
|
|
63
|
+
bundle exec rspec
|
|
64
|
+
bundle exec rubocop
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
Apache-2.0
|
data/lex-uais.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/uais/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-uais'
|
|
7
|
+
spec.version = Legion::Extensions::Uais::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['legionio@esity.info']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LegionIO extension for United AI Studio (UAIS) registration'
|
|
12
|
+
spec.description = 'UAIS agent registration client with mock adapter and soft-warn enforcement'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-uais'
|
|
14
|
+
spec.license = 'Apache-2.0'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
|
|
23
|
+
spec.require_paths = ['lib']
|
|
24
|
+
|
|
25
|
+
spec.add_dependency 'legion-json', '>= 1.2'
|
|
26
|
+
spec.add_dependency 'legion-logging', '>= 0.3'
|
|
27
|
+
spec.add_dependency 'legion-settings', '>= 0.3'
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Uais
|
|
6
|
+
class Client
|
|
7
|
+
include Helpers::Client
|
|
8
|
+
include Runners::Registration
|
|
9
|
+
|
|
10
|
+
attr_reader :opts
|
|
11
|
+
|
|
12
|
+
def initialize(base_url: nil, api_key: nil, timeout: 30, mock: nil, **extra)
|
|
13
|
+
mock = base_url.nil? if mock.nil?
|
|
14
|
+
@opts = { base_url: base_url, api_key: api_key, timeout: timeout, mock: mock, **extra }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def settings
|
|
18
|
+
{ options: @opts }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Uais
|
|
9
|
+
module Helpers
|
|
10
|
+
module Client
|
|
11
|
+
def connection(base_url:, api_key: nil, timeout: 30, **_opts)
|
|
12
|
+
uri = URI.parse(base_url)
|
|
13
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
14
|
+
http.use_ssl = uri.scheme == 'https'
|
|
15
|
+
http.open_timeout = timeout
|
|
16
|
+
http.read_timeout = timeout
|
|
17
|
+
http.instance_variable_set(:@_api_key, api_key)
|
|
18
|
+
|
|
19
|
+
def http.api_key
|
|
20
|
+
@_api_key
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
http
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_request(method, path, body: nil, api_key: nil)
|
|
27
|
+
klass = case method.to_s.downcase
|
|
28
|
+
when 'post' then Net::HTTP::Post
|
|
29
|
+
when 'put' then Net::HTTP::Put
|
|
30
|
+
when 'get' then Net::HTTP::Get
|
|
31
|
+
when 'delete' then Net::HTTP::Delete
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError, "unsupported HTTP method: #{method}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
req = klass.new(path)
|
|
37
|
+
req['Content-Type'] = 'application/json'
|
|
38
|
+
req['Authorization'] = "Bearer #{api_key}" if api_key
|
|
39
|
+
req.body = Legion::JSON.dump(body) if body
|
|
40
|
+
req
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Uais
|
|
6
|
+
module Runners
|
|
7
|
+
module Registration
|
|
8
|
+
def register_worker(worker_id:, name:, owner_msid:, extension_name:, **opts)
|
|
9
|
+
return mock_register(worker_id, name) if mock_mode?
|
|
10
|
+
|
|
11
|
+
payload = {
|
|
12
|
+
worker_id: worker_id,
|
|
13
|
+
name: name,
|
|
14
|
+
owner_msid: owner_msid,
|
|
15
|
+
extension_name: extension_name,
|
|
16
|
+
risk_tier: opts[:risk_tier],
|
|
17
|
+
team: opts[:team],
|
|
18
|
+
business_role: opts[:business_role]
|
|
19
|
+
}.compact
|
|
20
|
+
|
|
21
|
+
response = uais_post('/api/v1/agents', payload)
|
|
22
|
+
{ registered: response[:success], uais_id: response[:data]&.dig(:id), worker_id: worker_id }
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
soft_warn("UAIS registration failed for #{worker_id}: #{e.message}")
|
|
25
|
+
{ registered: false, worker_id: worker_id, error: e.message }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deregister_worker(worker_id:, reason: nil)
|
|
29
|
+
return mock_deregister(worker_id) if mock_mode?
|
|
30
|
+
|
|
31
|
+
response = uais_post("/api/v1/agents/#{worker_id}/deregister", { reason: reason })
|
|
32
|
+
{ deregistered: response[:success], worker_id: worker_id }
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
soft_warn("UAIS deregistration failed for #{worker_id}: #{e.message}")
|
|
35
|
+
{ deregistered: false, worker_id: worker_id, error: e.message }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_registration(worker_id:)
|
|
39
|
+
return { registered: true, mock: true } if mock_mode?
|
|
40
|
+
|
|
41
|
+
response = uais_get("/api/v1/agents/#{worker_id}")
|
|
42
|
+
{ registered: response[:success], data: response[:data] }
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
soft_warn("UAIS registration check failed for #{worker_id}: #{e.message}")
|
|
45
|
+
{ registered: false, error: e.message }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def airb_status(worker_id:)
|
|
49
|
+
return { status: 'approved', mock: true } if mock_mode?
|
|
50
|
+
|
|
51
|
+
response = uais_get("/api/v1/agents/#{worker_id}/airb")
|
|
52
|
+
{ status: response.dig(:data, :status), data: response[:data] }
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
soft_warn("UAIS AIRB check failed for #{worker_id}: #{e.message}")
|
|
55
|
+
{ status: 'unknown', error: e.message }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def uais_settings
|
|
61
|
+
settings[:options] || {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def uais_post(path, body)
|
|
65
|
+
conn = connection(**uais_connection_opts)
|
|
66
|
+
req = build_request(:post, path, body: body, api_key: uais_settings[:api_key])
|
|
67
|
+
resp = conn.request(req)
|
|
68
|
+
parse_response(resp)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def uais_get(path)
|
|
72
|
+
conn = connection(**uais_connection_opts)
|
|
73
|
+
req = build_request(:get, path, api_key: uais_settings[:api_key])
|
|
74
|
+
resp = conn.request(req)
|
|
75
|
+
parse_response(resp)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def uais_connection_opts
|
|
79
|
+
{
|
|
80
|
+
base_url: uais_settings[:base_url] || 'https://api.uais.uhg.com',
|
|
81
|
+
api_key: uais_settings[:api_key],
|
|
82
|
+
timeout: uais_settings[:timeout] || 30
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def parse_response(resp)
|
|
87
|
+
body = begin
|
|
88
|
+
Legion::JSON.load(resp.body)
|
|
89
|
+
rescue StandardError
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
{ success: resp.is_a?(Net::HTTPSuccess), status: resp.code.to_i, data: body }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def mock_mode?
|
|
96
|
+
uais_settings[:mock] == true || uais_settings[:base_url].nil?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def mock_register(worker_id, name)
|
|
100
|
+
soft_warn("UAIS mock mode: register #{name} (#{worker_id})")
|
|
101
|
+
{ registered: true, mock: true, worker_id: worker_id }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def mock_deregister(worker_id)
|
|
105
|
+
soft_warn("UAIS mock mode: deregister #{worker_id}")
|
|
106
|
+
{ deregistered: true, mock: true, worker_id: worker_id }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def soft_warn(message)
|
|
110
|
+
Legion::Logging.warn("[lex-uais] #{message}") if defined?(Legion::Logging)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
include Helpers::Client if defined?(Helpers::Client)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/uais/version'
|
|
4
|
+
require 'legion/extensions/uais/helpers/client'
|
|
5
|
+
require 'legion/extensions/uais/runners/registration'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Uais
|
|
10
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
11
|
+
|
|
12
|
+
def self.default_settings
|
|
13
|
+
{
|
|
14
|
+
options: {
|
|
15
|
+
base_url: nil,
|
|
16
|
+
api_key: nil,
|
|
17
|
+
mock: true,
|
|
18
|
+
timeout: 30
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/uais/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Uais::Client do
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'defaults to mock mode when no base_url' do
|
|
8
|
+
client = described_class.new
|
|
9
|
+
expect(client.opts[:mock]).to be true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defaults to mock mode when base_url is nil' do
|
|
13
|
+
client = described_class.new(base_url: nil)
|
|
14
|
+
expect(client.opts[:mock]).to be true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'disables mock when base_url is provided and mock not specified' do
|
|
18
|
+
client = described_class.new(base_url: 'https://example.com')
|
|
19
|
+
expect(client.opts[:mock]).to be false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'respects explicit mock: true even with base_url' do
|
|
23
|
+
client = described_class.new(base_url: 'https://example.com', mock: true)
|
|
24
|
+
expect(client.opts[:mock]).to be true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'stores api_key' do
|
|
28
|
+
client = described_class.new(api_key: 'test-key')
|
|
29
|
+
expect(client.opts[:api_key]).to eq('test-key')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'stores timeout' do
|
|
33
|
+
client = described_class.new(timeout: 60)
|
|
34
|
+
expect(client.opts[:timeout]).to eq(60)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#settings' do
|
|
39
|
+
it 'wraps opts in :options key' do
|
|
40
|
+
client = described_class.new
|
|
41
|
+
expect(client.settings).to have_key(:options)
|
|
42
|
+
expect(client.settings[:options]).to eq(client.opts)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/uais/helpers/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Uais::Helpers::Client do
|
|
6
|
+
let(:test_class) { Class.new { include Legion::Extensions::Uais::Helpers::Client } }
|
|
7
|
+
let(:instance) { test_class.new }
|
|
8
|
+
|
|
9
|
+
describe '#connection' do
|
|
10
|
+
it 'returns a Net::HTTP instance' do
|
|
11
|
+
conn = instance.connection(base_url: 'https://example.com')
|
|
12
|
+
expect(conn).to be_a(Net::HTTP)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'enables SSL for https' do
|
|
16
|
+
conn = instance.connection(base_url: 'https://example.com')
|
|
17
|
+
expect(conn.use_ssl?).to be true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'disables SSL for http' do
|
|
21
|
+
conn = instance.connection(base_url: 'http://example.com')
|
|
22
|
+
expect(conn.use_ssl?).to be false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'sets timeout' do
|
|
26
|
+
conn = instance.connection(base_url: 'https://example.com', timeout: 15)
|
|
27
|
+
expect(conn.open_timeout).to eq(15)
|
|
28
|
+
expect(conn.read_timeout).to eq(15)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#build_request' do
|
|
33
|
+
it 'creates a POST request' do
|
|
34
|
+
req = instance.build_request(:post, '/api/v1/agents')
|
|
35
|
+
expect(req).to be_a(Net::HTTP::Post)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'creates a GET request' do
|
|
39
|
+
req = instance.build_request(:get, '/api/v1/agents/123')
|
|
40
|
+
expect(req).to be_a(Net::HTTP::Get)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'sets content type to JSON' do
|
|
44
|
+
req = instance.build_request(:post, '/test')
|
|
45
|
+
expect(req['Content-Type']).to eq('application/json')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'sets authorization header when api_key provided' do
|
|
49
|
+
req = instance.build_request(:get, '/test', api_key: 'my-key')
|
|
50
|
+
expect(req['Authorization']).to eq('Bearer my-key')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'does not set authorization without api_key' do
|
|
54
|
+
req = instance.build_request(:get, '/test')
|
|
55
|
+
expect(req['Authorization']).to be_nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'serializes body as JSON' do
|
|
59
|
+
req = instance.build_request(:post, '/test', body: { name: 'TestBot' })
|
|
60
|
+
expect(req.body).to include('TestBot')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/uais/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Uais::Runners::Registration do
|
|
6
|
+
let(:client) { Legion::Extensions::Uais::Client.new }
|
|
7
|
+
let(:live_client) { Legion::Extensions::Uais::Client.new(base_url: 'https://api.uais.uhg.com', api_key: 'test-key', mock: false) }
|
|
8
|
+
|
|
9
|
+
describe 'mock mode' do
|
|
10
|
+
describe '#register_worker' do
|
|
11
|
+
subject(:result) do
|
|
12
|
+
client.register_worker(worker_id: 'w-123', name: 'TestBot', owner_msid: 'ms456', extension_name: 'lex-test')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'returns registered: true' do
|
|
16
|
+
expect(result[:registered]).to be true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'includes mock: true' do
|
|
20
|
+
expect(result[:mock]).to be true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'includes the worker_id' do
|
|
24
|
+
expect(result[:worker_id]).to eq('w-123')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#deregister_worker' do
|
|
29
|
+
subject(:result) { client.deregister_worker(worker_id: 'w-123') }
|
|
30
|
+
|
|
31
|
+
it 'returns deregistered: true' do
|
|
32
|
+
expect(result[:deregistered]).to be true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'includes mock: true' do
|
|
36
|
+
expect(result[:mock]).to be true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#check_registration' do
|
|
41
|
+
subject(:result) { client.check_registration(worker_id: 'w-123') }
|
|
42
|
+
|
|
43
|
+
it 'returns registered: true' do
|
|
44
|
+
expect(result[:registered]).to be true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'includes mock: true' do
|
|
48
|
+
expect(result[:mock]).to be true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#airb_status' do
|
|
53
|
+
subject(:result) { client.airb_status(worker_id: 'w-123') }
|
|
54
|
+
|
|
55
|
+
it 'returns status approved' do
|
|
56
|
+
expect(result[:status]).to eq('approved')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'includes mock: true' do
|
|
60
|
+
expect(result[:mock]).to be true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe 'mock_mode?' do
|
|
66
|
+
it 'is true when base_url is nil' do
|
|
67
|
+
c = Legion::Extensions::Uais::Client.new(base_url: nil)
|
|
68
|
+
expect(c.send(:mock_mode?)).to be true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'is true when mock is explicitly true' do
|
|
72
|
+
c = Legion::Extensions::Uais::Client.new(base_url: 'https://example.com', mock: true)
|
|
73
|
+
expect(c.send(:mock_mode?)).to be true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'is false when base_url is set and mock is false' do
|
|
77
|
+
c = Legion::Extensions::Uais::Client.new(base_url: 'https://example.com', mock: false)
|
|
78
|
+
expect(c.send(:mock_mode?)).to be false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe 'soft-warn on failure' do
|
|
83
|
+
it 'returns error hash instead of raising on register failure' do
|
|
84
|
+
allow(live_client).to receive(:connection).and_raise(Errno::ECONNREFUSED)
|
|
85
|
+
result = live_client.register_worker(worker_id: 'w-fail', name: 'FailBot', owner_msid: 'ms789', extension_name: 'lex-fail')
|
|
86
|
+
expect(result[:registered]).to be false
|
|
87
|
+
expect(result[:error]).to include('Connection refused')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'returns error hash instead of raising on deregister failure' do
|
|
91
|
+
allow(live_client).to receive(:connection).and_raise(Errno::ECONNREFUSED)
|
|
92
|
+
result = live_client.deregister_worker(worker_id: 'w-fail')
|
|
93
|
+
expect(result[:deregistered]).to be false
|
|
94
|
+
expect(result[:error]).to include('Connection refused')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns error hash instead of raising on check_registration failure' do
|
|
98
|
+
allow(live_client).to receive(:connection).and_raise(Errno::ECONNREFUSED)
|
|
99
|
+
result = live_client.check_registration(worker_id: 'w-fail')
|
|
100
|
+
expect(result[:registered]).to be false
|
|
101
|
+
expect(result[:error]).to include('Connection refused')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns error hash instead of raising on airb_status failure' do
|
|
105
|
+
allow(live_client).to receive(:connection).and_raise(Errno::ECONNREFUSED)
|
|
106
|
+
result = live_client.airb_status(worker_id: 'w-fail')
|
|
107
|
+
expect(result[:status]).to eq('unknown')
|
|
108
|
+
expect(result[:error]).to include('Connection refused')
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Uais do
|
|
4
|
+
it 'has a version number' do
|
|
5
|
+
expect(Legion::Extensions::Uais::VERSION).not_to be_nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'version is a string' do
|
|
9
|
+
expect(Legion::Extensions::Uais::VERSION).to be_a(String)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '.default_settings' do
|
|
13
|
+
subject(:settings) { described_class.default_settings }
|
|
14
|
+
|
|
15
|
+
it 'returns a hash with options' do
|
|
16
|
+
expect(settings).to be_a(Hash)
|
|
17
|
+
expect(settings).to have_key(:options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'defaults mock to true' do
|
|
21
|
+
expect(settings[:options][:mock]).to be true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'defaults base_url to nil' do
|
|
25
|
+
expect(settings[:options][:base_url]).to be_nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'defaults timeout to 30' do
|
|
29
|
+
expect(settings[:options][:timeout]).to eq(30)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
# Minimal stubs for Legion framework modules used by lex-uais
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Helpers
|
|
9
|
+
module Lex; end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Logging
|
|
14
|
+
def self.warn(msg); end
|
|
15
|
+
def self.info(msg); end
|
|
16
|
+
def self.debug(msg); end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module Settings
|
|
20
|
+
def self.[](_key)
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.dig(*_keys)
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.merge_settings(key, defaults); end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module JSON
|
|
32
|
+
def self.dump(obj)
|
|
33
|
+
require 'json'
|
|
34
|
+
::JSON.generate(obj)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.load(str)
|
|
38
|
+
require 'json'
|
|
39
|
+
::JSON.parse(str, symbolize_names: true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
require 'legion/extensions/uais'
|
|
45
|
+
|
|
46
|
+
RSpec.configure do |config|
|
|
47
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
48
|
+
config.disable_monkey_patching!
|
|
49
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
50
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-uais
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: legion-logging
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: legion-settings
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.3'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.3'
|
|
54
|
+
description: UAIS agent registration client with mock adapter and soft-warn enforcement
|
|
55
|
+
email:
|
|
56
|
+
- legionio@esity.info
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- ".github/CODEOWNERS"
|
|
62
|
+
- ".github/workflows/ci.yml"
|
|
63
|
+
- ".gitignore"
|
|
64
|
+
- ".rspec"
|
|
65
|
+
- ".rubocop.yml"
|
|
66
|
+
- CHANGELOG.md
|
|
67
|
+
- CLAUDE.md
|
|
68
|
+
- Gemfile
|
|
69
|
+
- README.md
|
|
70
|
+
- lex-uais.gemspec
|
|
71
|
+
- lib/legion/extensions/uais.rb
|
|
72
|
+
- lib/legion/extensions/uais/client.rb
|
|
73
|
+
- lib/legion/extensions/uais/helpers/client.rb
|
|
74
|
+
- lib/legion/extensions/uais/runners/registration.rb
|
|
75
|
+
- lib/legion/extensions/uais/version.rb
|
|
76
|
+
- spec/legion/extensions/uais/client_spec.rb
|
|
77
|
+
- spec/legion/extensions/uais/helpers/client_spec.rb
|
|
78
|
+
- spec/legion/extensions/uais/runners/registration_spec.rb
|
|
79
|
+
- spec/legion/extensions/uais_spec.rb
|
|
80
|
+
- spec/spec_helper.rb
|
|
81
|
+
homepage: https://github.com/LegionIO/lex-uais
|
|
82
|
+
licenses:
|
|
83
|
+
- Apache-2.0
|
|
84
|
+
metadata:
|
|
85
|
+
rubygems_mfa_required: 'true'
|
|
86
|
+
homepage_uri: https://github.com/LegionIO/lex-uais
|
|
87
|
+
source_code_uri: https://github.com/LegionIO/lex-uais
|
|
88
|
+
changelog_uri: https://github.com/LegionIO/lex-uais/blob/main/CHANGELOG.md
|
|
89
|
+
rdoc_options: []
|
|
90
|
+
require_paths:
|
|
91
|
+
- lib
|
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.4'
|
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
requirements: []
|
|
103
|
+
rubygems_version: 3.6.9
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: LegionIO extension for United AI Studio (UAIS) registration
|
|
106
|
+
test_files: []
|