ace-lab 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.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Lab
5
+ module Organisms
6
+ # Public query facade for all ace-lab CLI commands (spec 8wq.t.1w4):
7
+ # loads and validates topology, derives the verified caller identity,
8
+ # and returns classified QueryResults. Read-only by contract — it never
9
+ # invokes a service, grants credentials, or creates any state.
10
+ class TopologyService
11
+ # Build a service from configuration (ADR-022 cascade when omitted).
12
+ # Normalization happens lazily inside the classified boundary so
13
+ # configuration defects surface as invalid_configuration results.
14
+ def self.from_config(config = nil)
15
+ new(config: config)
16
+ end
17
+
18
+ # @param config [Hash, nil] pre-supplied configuration to normalize
19
+ # @param loader [Molecules::TopologyLoader, nil] injected loader
20
+ # @param authorizer [Molecules::CallerAuthorizer, nil] injected authorizer
21
+ # @raise [ArgumentError] when only one of loader/authorizer is
22
+ # injected — partial injection would silently mix collaborators
23
+ # (subject review: honor independently injected collaborators)
24
+ def initialize(config: nil, loader: nil, authorizer: nil)
25
+ if loader.nil? != authorizer.nil?
26
+ raise ArgumentError, "loader and authorizer must be injected together or both omitted"
27
+ end
28
+
29
+ @config = config
30
+ @loader = loader
31
+ @authorizer = authorizer
32
+ end
33
+
34
+ def projects
35
+ query { |index, authorizer| Molecules::InventoryQuery.new(index: index, authorizer: authorizer).projects }
36
+ end
37
+
38
+ def agents(project:)
39
+ query do |index, authorizer|
40
+ Molecules::InventoryQuery.new(index: index, authorizer: authorizer).agents(project: project)
41
+ end
42
+ end
43
+
44
+ def services(project:)
45
+ query do |index, authorizer|
46
+ Molecules::InventoryQuery.new(index: index, authorizer: authorizer).services(project: project)
47
+ end
48
+ end
49
+
50
+ def resolve(id:)
51
+ query { |index, authorizer| Molecules::ExactResolver.new(index: index, authorizer: authorizer).resolve(id) }
52
+ end
53
+
54
+ def route(project:, capability:)
55
+ query do |index, authorizer|
56
+ Molecules::CapabilityRouter.new(index: index, authorizer: authorizer)
57
+ .route(project: project, capability: capability)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Run a query against the validated index. Configuration loading,
64
+ # schema validation, and authorization derivation all happen inside
65
+ # this boundary so every defect classifies as invalid_configuration
66
+ # (review R4) and authorization always uses the normalized document
67
+ # the topology was validated against (review R3).
68
+ def query
69
+ index, authorizer = dependencies
70
+ yield index, authorizer
71
+ rescue Ace::Lab::InvalidConfigurationError => e
72
+ Models::QueryResult.failure("invalid_configuration", e.message)
73
+ end
74
+
75
+ # Dependencies are derived fresh for EVERY query: nothing is
76
+ # memoized, so revoking deployment grants revokes access on the next
77
+ # query even for a reused service instance (review round 6, F1).
78
+ def dependencies
79
+ return [@loader.load, @authorizer] if @loader
80
+
81
+ config = @config || Ace::Lab.config
82
+ normalized = Atoms::TopologySchema.normalize!(config)
83
+ grants = if @config
84
+ # Explicitly injected configuration is a trusted in-process
85
+ # input (API use and fixtures)
86
+ normalized["authorization"]["principals"]
87
+ else
88
+ # Cascade path: grants come only from the trusted deployment-
89
+ # controlled document, re-read and re-verified per query; cascade
90
+ # tiers can never define or expand authorization (review round 4, F3)
91
+ Molecules::GrantResolver.resolve(
92
+ documents: Ace::Lab.cascade_documents,
93
+ topology: normalized["topology"],
94
+ trusted_path: Ace::Lab.authorization_path
95
+ )["principals"]
96
+ end
97
+
98
+ loader = Molecules::TopologyLoader.new(normalized)
99
+ authorizer = Molecules::CallerAuthorizer.new(principals: grants)
100
+ [loader.load, authorizer]
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Lab
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/ace/lab.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lab/version"
4
+ require "ace/support/config"
5
+
6
+ module Ace
7
+ module Lab
8
+ class Error < StandardError; end
9
+
10
+ # Raised when topology configuration violates the schema contract
11
+ class InvalidConfigurationError < Error; end
12
+ end
13
+ end
14
+
15
+ # Load all ace-lab components
16
+ require_relative "lab/atoms/topology_schema"
17
+ require_relative "lab/atoms/binding_freshness"
18
+ require_relative "lab/atoms/public_projection"
19
+ require_relative "lab/models/runtime_binding"
20
+ require_relative "lab/models/topology_entry"
21
+ require_relative "lab/models/query_result"
22
+ require_relative "lab/molecules/topology_loader"
23
+ require_relative "lab/molecules/caller_authorizer"
24
+ require_relative "lab/molecules/grant_resolver"
25
+ require_relative "lab/molecules/inventory_query"
26
+ require_relative "lab/molecules/exact_resolver"
27
+ require_relative "lab/molecules/capability_router"
28
+ require_relative "lab/organisms/topology_service"
29
+ require_relative "lab/cli"
30
+
31
+ module Ace
32
+ module Lab
33
+ # Returns the gem root directory
34
+ # @return [String] Path to the gem root directory
35
+ def self.gem_root
36
+ @gem_root ||= Gem.loaded_specs["ace-lab"]&.gem_dir ||
37
+ File.expand_path("../..", __dir__)
38
+ end
39
+
40
+ # Check if debug mode is enabled
41
+ # @return [Boolean]
42
+ def self.debug?
43
+ ENV["ACE_DEBUG"] == "1" || ENV["DEBUG"] == "1"
44
+ end
45
+
46
+ # Load ace-lab configuration using the ADR-022 cascade.
47
+ # Deployed topology lives in .ace/lab/config.yml (project) or
48
+ # ~/.ace/lab/config.yml (user); gem defaults are an empty, safe topology.
49
+ # Deliberately NOT memoized: binding facts (attestations) change on disk,
50
+ # and a long-lived consumer must never keep serving replaced bindings
51
+ # from a cached document (review round 7, F1). Cascade reads are small.
52
+ # @return [Hash] Merged configuration hash with defaults
53
+ def self.config
54
+ load_config
55
+ end
56
+
57
+ # Same discovery the cascade resolver performs for this namespace
58
+ LAB_FILE_PATTERNS = ["lab/config.yml", "lab/config.yaml"].freeze
59
+
60
+ # Authorization grants live in a single deployment-controlled file at a
61
+ # FIXED path — never in the caller-writable configuration cascade and
62
+ # never at a caller-selected location (review rounds 4-5, F3/F1). The
63
+ # file must be root-owned and not group/world-writable, including every
64
+ # directory on its real path; GrantResolver verifies and fails closed.
65
+ AUTHORIZATION_PATH = "/etc/lab/ace-lab/authorization.yml"
66
+
67
+ # Path of the trusted authorization grants document. A method (not a
68
+ # bare constant reference) so tests can stub the seam without any
69
+ # caller-controllable production override.
70
+ # @return [String]
71
+ def self.authorization_path
72
+ AUTHORIZATION_PATH
73
+ end
74
+
75
+ # Load configuration using Ace::Support::Config cascade.
76
+ # Load failures raise InvalidConfigurationError (classified by the query
77
+ # boundary) — silently falling back to empty defaults would misreport
78
+ # broken deployed configuration as an authorization denial (review R4).
79
+ # @return [Hash] Merged configuration
80
+ # @raise [Ace::Lab::InvalidConfigurationError]
81
+ def self.load_config
82
+ cascade_documents
83
+
84
+ resolver = Ace::Support::Config.create(
85
+ config_dir: ".ace",
86
+ defaults_dir: ".ace-defaults",
87
+ gem_path: gem_root
88
+ )
89
+
90
+ resolver.resolve_namespace("lab").data
91
+ rescue Ace::Lab::InvalidConfigurationError
92
+ raise
93
+ rescue => e
94
+ # Fixed diagnostic only: the underlying exception message may quote
95
+ # configuration content, which must never surface pre-authorization
96
+ # (review round 3, F2)
97
+ raise InvalidConfigurationError, "invalid lab configuration: could not load deployed configuration (#{e.class})"
98
+ end
99
+ private_class_method :load_config
100
+
101
+ # The individual cascade documents for this namespace, same discovery the
102
+ # resolver performs, each validated before use. Cascade merging treats a
103
+ # non-mapping document as an empty overlay, so a malformed root (e.g. a
104
+ # YAML array) would be silently ignored (review round 3, F1). Messages
105
+ # carry the file path — never parsed content.
106
+ #
107
+ # Exposed because authorization grants need per-tier documents: a
108
+ # caller-writable tier must never be able to EXPAND grants (review
109
+ # round 4, F3), which the merged view cannot express.
110
+ #
111
+ # @return [Array<Hash>] {path:, document:, defaults:}
112
+ def self.cascade_documents
113
+ finder = Ace::Support::Config::Molecules::ConfigFinder.new(
114
+ config_dir: ".ace",
115
+ defaults_dir: ".ace-defaults",
116
+ gem_path: gem_root,
117
+ file_patterns: LAB_FILE_PATTERNS
118
+ )
119
+
120
+ finder.find_all.select(&:exists).map do |cascade_path|
121
+ path = cascade_path.path
122
+ document = begin
123
+ require "yaml"
124
+ YAML.safe_load_file(path, permitted_classes: [Date], aliases: true)
125
+ rescue
126
+ raise InvalidConfigurationError, "invalid lab configuration: #{path} could not be parsed as YAML"
127
+ end
128
+ unless document.nil? || document.is_a?(Hash)
129
+ raise InvalidConfigurationError, "invalid lab configuration: #{path} must contain a YAML mapping"
130
+ end
131
+
132
+ {path: path, document: document || {}, defaults: path.start_with?(gem_root)}
133
+ end
134
+ end
135
+ end
136
+ end
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ace-lab
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michal Czyz
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2026-09-29 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ace-support-cli
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.6'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ace-support-core
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.31'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.31'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ace-support-config
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.18'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.18'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ace-support-test-helpers
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.14'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.14'
68
+ - !ruby/object:Gem::Dependency
69
+ name: bundler
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: minitest
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '5.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '5.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '13.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubocop
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.88'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.88'
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.22'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.22'
138
+ description: Read-only topology registry and capability router. Resolves stable project,
139
+ agent, and service IDs from ADR-022 configuration without invoking Lab, reading
140
+ credentials, tracking work, or guessing transient pane/session identifiers.
141
+ email:
142
+ - mc@cs3b.com
143
+ executables:
144
+ - ace-lab
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - ".ace-defaults/lab/config.yml"
149
+ - CHANGELOG.md
150
+ - LICENSE
151
+ - README.md
152
+ - Rakefile
153
+ - docs/usage.md
154
+ - exe/ace-lab
155
+ - lib/ace/lab.rb
156
+ - lib/ace/lab/atoms/binding_freshness.rb
157
+ - lib/ace/lab/atoms/public_projection.rb
158
+ - lib/ace/lab/atoms/topology_schema.rb
159
+ - lib/ace/lab/cli.rb
160
+ - lib/ace/lab/cli/commands/agents.rb
161
+ - lib/ace/lab/cli/commands/projects.rb
162
+ - lib/ace/lab/cli/commands/resolve.rb
163
+ - lib/ace/lab/cli/commands/route.rb
164
+ - lib/ace/lab/cli/commands/services.rb
165
+ - lib/ace/lab/cli/commands/support.rb
166
+ - lib/ace/lab/models/query_result.rb
167
+ - lib/ace/lab/models/runtime_binding.rb
168
+ - lib/ace/lab/models/topology_entry.rb
169
+ - lib/ace/lab/molecules/caller_authorizer.rb
170
+ - lib/ace/lab/molecules/capability_router.rb
171
+ - lib/ace/lab/molecules/exact_resolver.rb
172
+ - lib/ace/lab/molecules/grant_resolver.rb
173
+ - lib/ace/lab/molecules/inventory_query.rb
174
+ - lib/ace/lab/molecules/topology_loader.rb
175
+ - lib/ace/lab/organisms/topology_service.rb
176
+ - lib/ace/lab/version.rb
177
+ homepage: https://github.com/cs3b/ace
178
+ licenses:
179
+ - MIT
180
+ metadata:
181
+ allowed_push_host: https://rubygems.org
182
+ homepage_uri: https://github.com/cs3b/ace
183
+ source_code_uri: https://github.com/cs3b/ace/tree/main/ace-lab/
184
+ changelog_uri: https://github.com/cs3b/ace/blob/main/ace-lab/CHANGELOG.md
185
+ rdoc_options: []
186
+ require_paths:
187
+ - lib
188
+ required_ruby_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: 3.2.0
193
+ required_rubygems_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ requirements: []
199
+ rubygems_version: 3.6.9
200
+ specification_version: 4
201
+ summary: 'Topology and routing CLI for the Lab: address projects, agents, and services
202
+ by stable IDs'
203
+ test_files: []