ace-lab 0.13.1 → 0.14.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 +4 -4
- data/CHANGELOG.md +94 -1
- data/docs/usage.md +130 -0
- data/lib/ace/lab/atoms/install_schema.rb +72 -11
- data/lib/ace/lab/auth/credential_handoff.rb +47 -0
- data/lib/ace/lab/auth/lease_store.rb +212 -0
- data/lib/ace/lab/auth/plugins/env_provider.rb +60 -0
- data/lib/ace/lab/auth/plugins/forgejo_provider.rb +189 -0
- data/lib/ace/lab/auth/plugins/github_provider.rb +207 -0
- data/lib/ace/lab/auth/profile_schema.rb +152 -0
- data/lib/ace/lab/auth/providers.rb +66 -0
- data/lib/ace/lab/auth/service.rb +457 -0
- data/lib/ace/lab/auth.rb +25 -0
- data/lib/ace/lab/cli/commands/auth.rb +99 -0
- data/lib/ace/lab/cli.rb +4 -1
- data/lib/ace/lab/molecules/extension_projection.rb +91 -0
- data/lib/ace/lab/molecules/extension_provisioner.rb +226 -0
- data/lib/ace/lab/molecules/gemset_provisioner.rb +223 -4
- data/lib/ace/lab/molecules/project_locations.rb +62 -0
- data/lib/ace/lab/organisms/prepare.rb +28 -2
- data/lib/ace/lab/organisms/verify.rb +139 -9
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +1 -0
- metadata +15 -2
|
@@ -7,6 +7,7 @@ require_relative "../molecules/directory_provisioner"
|
|
|
7
7
|
require_relative "../molecules/key_provisioner"
|
|
8
8
|
require_relative "../molecules/role_verifier"
|
|
9
9
|
require_relative "../molecules/gemset_provisioner"
|
|
10
|
+
require_relative "../molecules/extension_provisioner"
|
|
10
11
|
|
|
11
12
|
module Ace
|
|
12
13
|
module Lab
|
|
@@ -51,9 +52,11 @@ module Ace
|
|
|
51
52
|
unresolved, effective_dry_run)
|
|
52
53
|
roles = prepare_roles(manifest["roles"], available, unresolved, effective_dry_run)
|
|
53
54
|
gems = prepare_gems(manifest["gemset"], effective_dry_run)
|
|
55
|
+
extensions = prepare_extensions(manifest["gemset"], effective_dry_run, gems)
|
|
54
56
|
|
|
55
57
|
{
|
|
56
|
-
"ok" => [accounts, directories, keys, roles, gems].flatten
|
|
58
|
+
"ok" => [accounts, directories, keys, roles, gems, extensions].flatten
|
|
59
|
+
.none? { |item| item["status"] == "failed" },
|
|
57
60
|
"dry_run" => effective_dry_run,
|
|
58
61
|
"machine" => manifest["machine"],
|
|
59
62
|
"manifest" => manifest_path,
|
|
@@ -61,7 +64,8 @@ module Ace
|
|
|
61
64
|
"directories" => directories,
|
|
62
65
|
"keys" => keys,
|
|
63
66
|
"roles" => roles,
|
|
64
|
-
"gems" => gems
|
|
67
|
+
"gems" => gems,
|
|
68
|
+
"extensions" => extensions
|
|
65
69
|
}
|
|
66
70
|
end
|
|
67
71
|
|
|
@@ -152,6 +156,28 @@ module Ace
|
|
|
152
156
|
provisioner.call(runtime: runtime)
|
|
153
157
|
end
|
|
154
158
|
|
|
159
|
+
# Harness extension projections (task 8xs.t.pu0.0): converged
|
|
160
|
+
# from the installed gem's exact declared version after the
|
|
161
|
+
# gemset phase; a gem phase failure fails every extension as a
|
|
162
|
+
# dependency (the projection source would not exist).
|
|
163
|
+
def prepare_extensions(runtime, dry_run, gems)
|
|
164
|
+
entries = runtime["extensions"]
|
|
165
|
+
return [] if entries.empty?
|
|
166
|
+
|
|
167
|
+
if gems.any? { |gem| gem["status"] == "failed" }
|
|
168
|
+
return entries.map do |entry|
|
|
169
|
+
{"extension" => entry["gem"], "gem" => entry["gem"], "version" => nil,
|
|
170
|
+
"path" => entry["path"], "account" => entry["account"], "status" => "failed",
|
|
171
|
+
"loaded_version" => nil, "restart_required" => false, "files" => [],
|
|
172
|
+
"message" => "gemset convergence failed; fix the gem phase first"}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
Molecules::ExtensionProvisioner.new(
|
|
177
|
+
gem_home: runtime["gem_home"], dry_run: dry_run, account_lookup: @account_lookup
|
|
178
|
+
).call(extensions: entries, gems: runtime["gems"])
|
|
179
|
+
end
|
|
180
|
+
|
|
155
181
|
def available_accounts(account_reports)
|
|
156
182
|
account_reports.reject { |report| report["status"] == "failed" }
|
|
157
183
|
.map { |report| report["username"] }
|
|
@@ -5,11 +5,14 @@ require_relative "../molecules/install_manifest_loader"
|
|
|
5
5
|
require_relative "../molecules/key_provisioner"
|
|
6
6
|
require_relative "../molecules/role_verifier"
|
|
7
7
|
require_relative "../molecules/gemset_provisioner"
|
|
8
|
+
require_relative "../molecules/extension_provisioner"
|
|
8
9
|
require_relative "../molecules/hitl_routes"
|
|
9
10
|
require_relative "../molecules/caller_authorizer"
|
|
10
11
|
require_relative "../atoms/topology_schema"
|
|
11
12
|
require "open3"
|
|
12
13
|
require "yaml"
|
|
14
|
+
require "json"
|
|
15
|
+
require "fileutils"
|
|
13
16
|
require "date"
|
|
14
17
|
|
|
15
18
|
module Ace
|
|
@@ -99,7 +102,8 @@ module Ace
|
|
|
99
102
|
"overseer" => check_overseer(manifest),
|
|
100
103
|
"adapters" => check_adapters(manifest, executors),
|
|
101
104
|
"tools" => check_tools(manifest, executors),
|
|
102
|
-
"gems" => check_gems(manifest)
|
|
105
|
+
"gems" => check_gems(manifest),
|
|
106
|
+
"extensions" => check_extensions(manifest, entries)
|
|
103
107
|
}
|
|
104
108
|
flat = checks.values.flatten(1)
|
|
105
109
|
{
|
|
@@ -583,20 +587,146 @@ module Ace
|
|
|
583
587
|
kind, versions_or_message = provisioner.runtime_versions(
|
|
584
588
|
name: gem["name"], gem_home: gem_home
|
|
585
589
|
)
|
|
586
|
-
reason =
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
status = case kind
|
|
591
|
-
when :ok then reason ? FAIL : PASS
|
|
592
|
-
when :unknown then UNKNOWN
|
|
593
|
-
else FAIL
|
|
590
|
+
status, reason = if kind != :ok
|
|
591
|
+
[kind == :unknown ? UNKNOWN : FAIL, versions_or_message]
|
|
592
|
+
else
|
|
593
|
+
verify_present_gem(provisioner, gem, versions_or_message, gem_home)
|
|
594
594
|
end
|
|
595
595
|
{"gem" => gem["name"], "version" => gem["version"], "gem_home" => gem_home,
|
|
596
596
|
"status" => status, "reason" => reason}
|
|
597
597
|
end
|
|
598
598
|
end
|
|
599
599
|
|
|
600
|
+
# A gem whose exact declared version is present must ALSO prove
|
|
601
|
+
# (1) its runtime dependency closure resolves in the target
|
|
602
|
+
# runtime — a gem listed in `gem list` with a missing
|
|
603
|
+
# transitive dependency is NOT ready (task 8xs.t.pu0.0) — and
|
|
604
|
+
# (2) no NEWER version sits beside the declared one: RubyGems
|
|
605
|
+
# activates the highest installed version, so a newer sibling
|
|
606
|
+
# would silently take over the runtime's CLI.
|
|
607
|
+
def verify_present_gem(provisioner, gem, versions, gem_home)
|
|
608
|
+
reason = gem_version_reason(gem, versions, gem_home)
|
|
609
|
+
return [FAIL, reason] if reason
|
|
610
|
+
|
|
611
|
+
newer_kind, newer_or_message = provisioner.newer_versions(
|
|
612
|
+
name: gem["name"], declared_version: gem["version"], gem_home: gem_home
|
|
613
|
+
)
|
|
614
|
+
if newer_kind != :ok
|
|
615
|
+
return [newer_kind == :unknown ? UNKNOWN : FAIL, newer_or_message]
|
|
616
|
+
end
|
|
617
|
+
unless newer_or_message.empty?
|
|
618
|
+
return [FAIL,
|
|
619
|
+
"gem '#{gem["name"]}' has newer version(s) #{newer_or_message.join(", ")} beside " \
|
|
620
|
+
"the declared #{gem["version"]} in '#{gem_home}'; the newer version would take " \
|
|
621
|
+
"over CLI activation; rerun ace-lab install"]
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
closure_kind, closure_or_message = provisioner.closure_failures(
|
|
625
|
+
name: gem["name"], version: gem["version"], gem_home: gem_home
|
|
626
|
+
)
|
|
627
|
+
return [closure_kind == :unknown ? UNKNOWN : FAIL, closure_or_message] if closure_kind != :ok
|
|
628
|
+
return [FAIL, closure_or_message.join("; ")] unless closure_or_message.empty?
|
|
629
|
+
|
|
630
|
+
[PASS, nil]
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Declared harness extension projections (task 8xs.t.pu0.0):
|
|
634
|
+
# re-derived from the same receipts and installed gem trees the
|
|
635
|
+
# provisioner converges (shared ExtensionProjection helpers) —
|
|
636
|
+
# the receipt must name the declared gem at its exact declared
|
|
637
|
+
# version, every projected file must match the installed gem's
|
|
638
|
+
# handbook/extensions content, the destination must not escape
|
|
639
|
+
# the extensions directory through a symlinked component, and
|
|
640
|
+
# the projected files must grant the declared account read.
|
|
641
|
+
def check_extensions(manifest, entries)
|
|
642
|
+
gemset = manifest["gemset"]
|
|
643
|
+
gemset["extensions"].map do |entry|
|
|
644
|
+
declared = gemset["gems"].find { |gem| gem["name"] == entry["gem"] }
|
|
645
|
+
reason = if declared.nil?
|
|
646
|
+
"extension source gem '#{entry["gem"]}' is not declared in the gemset"
|
|
647
|
+
else
|
|
648
|
+
extension_reason(entry, declared, gemset["gem_home"], entries[entry["account"]])
|
|
649
|
+
end
|
|
650
|
+
{"extension" => entry["gem"], "gem" => entry["gem"], "path" => entry["path"],
|
|
651
|
+
"account" => entry["account"], "status" => reason ? FAIL : PASS, "reason" => reason}
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def extension_reason(entry, declared, gem_home, account_entry)
|
|
656
|
+
source_root = Molecules::ExtensionProjection.gem_source_root(
|
|
657
|
+
gem_home, entry["gem"], declared["version"]
|
|
658
|
+
)
|
|
659
|
+
return "gem '#{entry["gem"]}' #{declared["version"]} is not installed with a " \
|
|
660
|
+
"handbook/extensions tree in '#{gem_home}'; rerun ace-lab install" unless File.directory?(source_root)
|
|
661
|
+
|
|
662
|
+
receipt_path = File.join(entry["path"], Molecules::ExtensionProvisioner::RECEIPT_NAME)
|
|
663
|
+
unless File.file?(receipt_path)
|
|
664
|
+
return "extension projection receipt '#{receipt_path}' is missing; rerun ace-lab install"
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
begin
|
|
668
|
+
receipt = JSON.parse(File.read(receipt_path))
|
|
669
|
+
rescue JSON::ParserError
|
|
670
|
+
return "extension projection receipt '#{receipt_path}' is not valid JSON; rerun ace-lab install"
|
|
671
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
672
|
+
return "extension projection receipt '#{receipt_path}' is not readable; " \
|
|
673
|
+
"rerun ace-lab install"
|
|
674
|
+
end
|
|
675
|
+
if receipt["gem"] != entry["gem"] || receipt["version"] != declared["version"]
|
|
676
|
+
return "extension projection at '#{entry["path"]}' was projected from " \
|
|
677
|
+
"'#{receipt["gem"]} #{receipt["version"]}' but the manifest declares " \
|
|
678
|
+
"'#{entry["gem"]} #{declared["version"]}'; rerun ace-lab install"
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
expected = Molecules::ExtensionProjection.source_files(source_root)
|
|
682
|
+
listed = receipt.fetch("files", [])
|
|
683
|
+
if listed.sort != expected.keys.sort
|
|
684
|
+
return "extension projection at '#{entry["path"]}' lists different files than " \
|
|
685
|
+
"gem '#{entry["gem"]}' #{declared["version"]} carries; rerun ace-lab install"
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# A destination that escapes the extensions directory through
|
|
689
|
+
# a symlinked component is refused before any comparison (the
|
|
690
|
+
# same validate_destination posture convergence applies).
|
|
691
|
+
begin
|
|
692
|
+
expected.each_key do |relative|
|
|
693
|
+
Molecules::ExtensionProjection.validate_destination!(entry["path"], relative)
|
|
694
|
+
end
|
|
695
|
+
rescue Ace::Lab::Error => e
|
|
696
|
+
return "#{e.message}; rerun ace-lab install"
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
mismatch = expected.find do |relative, source|
|
|
700
|
+
target = File.join(entry["path"], relative)
|
|
701
|
+
!File.file?(target) || !FileUtils.compare_file(source, target)
|
|
702
|
+
end
|
|
703
|
+
return "extension projection file '#{mismatch.first}' differs from gem " \
|
|
704
|
+
"'#{entry["gem"]}' #{declared["version"]}; rerun ace-lab install" if mismatch
|
|
705
|
+
|
|
706
|
+
# Declared-account readability (stat-based — the verifier
|
|
707
|
+
# does not run as the declared account): a projected file is
|
|
708
|
+
# readable when it is world-readable, or owner-readable AND
|
|
709
|
+
# owned by the declared account's uid.
|
|
710
|
+
unreadable = expected.keys.find do |relative|
|
|
711
|
+
unreadable_reason(File.join(entry["path"], relative), account_entry)
|
|
712
|
+
end
|
|
713
|
+
return "extension projection file '#{unreadable}' is not readable by the declared " \
|
|
714
|
+
"account '#{entry["account"]}'; rerun ace-lab install" if unreadable
|
|
715
|
+
|
|
716
|
+
nil
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def unreadable_reason(path, account_entry)
|
|
720
|
+
stat = lstat(path)
|
|
721
|
+
return true if stat.nil? || !stat.file? || stat.symlink?
|
|
722
|
+
|
|
723
|
+
mode = stat.mode & 0o777
|
|
724
|
+
return false if (mode & 0o004) != 0 # world-readable
|
|
725
|
+
return false if (mode & 0o400) != 0 && account_entry && stat.uid == account_entry.uid
|
|
726
|
+
|
|
727
|
+
true
|
|
728
|
+
end
|
|
729
|
+
|
|
600
730
|
def gem_version_reason(gem, versions, gem_home)
|
|
601
731
|
return nil if versions.include?(gem["version"])
|
|
602
732
|
if versions.empty?
|
data/lib/ace/lab/version.rb
CHANGED
data/lib/ace/lab.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "lab/models/topology_entry"
|
|
|
18
18
|
require_relative "lab/models/query_result"
|
|
19
19
|
require_relative "lab/models/status_report"
|
|
20
20
|
require_relative "lab/molecules/topology_loader"
|
|
21
|
+
require_relative "lab/molecules/project_locations"
|
|
21
22
|
require_relative "lab/molecules/config_state_classifier"
|
|
22
23
|
require_relative "lab/molecules/lab_state_scanner"
|
|
23
24
|
require_relative "lab/molecules/caller_authorizer"
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-lab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -173,9 +173,19 @@ files:
|
|
|
173
173
|
- lib/ace/lab/atoms/install_schema.rb
|
|
174
174
|
- lib/ace/lab/atoms/public_projection.rb
|
|
175
175
|
- lib/ace/lab/atoms/topology_schema.rb
|
|
176
|
+
- lib/ace/lab/auth.rb
|
|
177
|
+
- lib/ace/lab/auth/credential_handoff.rb
|
|
178
|
+
- lib/ace/lab/auth/lease_store.rb
|
|
179
|
+
- lib/ace/lab/auth/plugins/env_provider.rb
|
|
180
|
+
- lib/ace/lab/auth/plugins/forgejo_provider.rb
|
|
181
|
+
- lib/ace/lab/auth/plugins/github_provider.rb
|
|
182
|
+
- lib/ace/lab/auth/profile_schema.rb
|
|
183
|
+
- lib/ace/lab/auth/providers.rb
|
|
184
|
+
- lib/ace/lab/auth/service.rb
|
|
176
185
|
- lib/ace/lab/authorization_path.rb
|
|
177
186
|
- lib/ace/lab/cli.rb
|
|
178
187
|
- lib/ace/lab/cli/commands/agents.rb
|
|
188
|
+
- lib/ace/lab/cli/commands/auth.rb
|
|
179
189
|
- lib/ace/lab/cli/commands/install.rb
|
|
180
190
|
- lib/ace/lab/cli/commands/prepare.rb
|
|
181
191
|
- lib/ace/lab/cli/commands/project_add.rb
|
|
@@ -198,6 +208,8 @@ files:
|
|
|
198
208
|
- lib/ace/lab/molecules/config_state_classifier.rb
|
|
199
209
|
- lib/ace/lab/molecules/directory_provisioner.rb
|
|
200
210
|
- lib/ace/lab/molecules/exact_resolver.rb
|
|
211
|
+
- lib/ace/lab/molecules/extension_projection.rb
|
|
212
|
+
- lib/ace/lab/molecules/extension_provisioner.rb
|
|
201
213
|
- lib/ace/lab/molecules/forge_merge_config.rb
|
|
202
214
|
- lib/ace/lab/molecules/forgejo_merge_adapter.rb
|
|
203
215
|
- lib/ace/lab/molecules/gemset_provisioner.rb
|
|
@@ -209,6 +221,7 @@ files:
|
|
|
209
221
|
- lib/ace/lab/molecules/key_provisioner.rb
|
|
210
222
|
- lib/ace/lab/molecules/lab_state_scanner.rb
|
|
211
223
|
- lib/ace/lab/molecules/overseer_provisioner.rb
|
|
224
|
+
- lib/ace/lab/molecules/project_locations.rb
|
|
212
225
|
- lib/ace/lab/molecules/project_registry.rb
|
|
213
226
|
- lib/ace/lab/molecules/repository_provisioner.rb
|
|
214
227
|
- lib/ace/lab/molecules/role_verifier.rb
|