ace-lab 0.13.0 → 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 +132 -1
- data/docs/usage.md +168 -4
- data/lib/ace/lab/atoms/install_schema.rb +135 -3
- 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/commands/prepare.rb +4 -2
- data/lib/ace/lab/cli/commands/verify.rb +2 -2
- 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 +441 -0
- data/lib/ace/lab/molecules/project_locations.rb +62 -0
- data/lib/ace/lab/organisms/install.rb +13 -8
- data/lib/ace/lab/organisms/prepare.rb +44 -4
- data/lib/ace/lab/organisms/verify.rb +200 -2
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +1 -0
- metadata +16 -2
|
@@ -4,11 +4,15 @@ require_relative "../errors"
|
|
|
4
4
|
require_relative "../molecules/install_manifest_loader"
|
|
5
5
|
require_relative "../molecules/key_provisioner"
|
|
6
6
|
require_relative "../molecules/role_verifier"
|
|
7
|
+
require_relative "../molecules/gemset_provisioner"
|
|
8
|
+
require_relative "../molecules/extension_provisioner"
|
|
7
9
|
require_relative "../molecules/hitl_routes"
|
|
8
10
|
require_relative "../molecules/caller_authorizer"
|
|
9
11
|
require_relative "../atoms/topology_schema"
|
|
10
12
|
require "open3"
|
|
11
13
|
require "yaml"
|
|
14
|
+
require "json"
|
|
15
|
+
require "fileutils"
|
|
12
16
|
require "date"
|
|
13
17
|
|
|
14
18
|
module Ace
|
|
@@ -77,7 +81,7 @@ module Ace
|
|
|
77
81
|
# @return [Hash] {ready:, machine:, manifest:,
|
|
78
82
|
# authorization_path:, checks: {accounts:, directories:,
|
|
79
83
|
# keys:, roles:, repositories:, grants:, registry:,
|
|
80
|
-
# overseer:, adapters:, tools:}, counts: {pass:, fail:,
|
|
84
|
+
# overseer:, adapters:, tools:, gems:}, counts: {pass:, fail:,
|
|
81
85
|
# unknown:}}
|
|
82
86
|
# @raise [Ace::Lab::InvalidConfigurationError] missing,
|
|
83
87
|
# untrusted or schema-invalid manifest — same classification
|
|
@@ -97,7 +101,9 @@ module Ace
|
|
|
97
101
|
"registry" => check_registry(manifest),
|
|
98
102
|
"overseer" => check_overseer(manifest),
|
|
99
103
|
"adapters" => check_adapters(manifest, executors),
|
|
100
|
-
"tools" => check_tools(manifest, executors)
|
|
104
|
+
"tools" => check_tools(manifest, executors),
|
|
105
|
+
"gems" => check_gems(manifest),
|
|
106
|
+
"extensions" => check_extensions(manifest, entries)
|
|
101
107
|
}
|
|
102
108
|
flat = checks.values.flatten(1)
|
|
103
109
|
{
|
|
@@ -551,6 +557,198 @@ module Ace
|
|
|
551
557
|
end
|
|
552
558
|
end
|
|
553
559
|
|
|
560
|
+
# Declared gem set (gad.2 gap #1, owner spec 8xg.t.sbz.7): every
|
|
561
|
+
# declared gem must be installed at its EXACT declared version in
|
|
562
|
+
# the declared target runtime (GEM_HOME) — read-only, through the
|
|
563
|
+
# SAME probe the provisioner converges from
|
|
564
|
+
# (GemsetProvisioner#runtime_versions), so readiness and
|
|
565
|
+
# convergence can never drift. A missing, symlinked or
|
|
566
|
+
# non-directory runtime fails every declared gem as a dependency
|
|
567
|
+
# (nothing could have converged into it); a gem at a wrong or
|
|
568
|
+
# missing version is a classified failure with an
|
|
569
|
+
# `ace-lab install` remedy; an environment without the `gem`
|
|
570
|
+
# executable degrades to unknown (the offline posture) and does
|
|
571
|
+
# not block by itself. Extra undeclared versions are not judged:
|
|
572
|
+
# the manifest is the single owner of the DECLARED set.
|
|
573
|
+
def check_gems(manifest)
|
|
574
|
+
gemset = manifest["gemset"]
|
|
575
|
+
gems = gemset["gems"]
|
|
576
|
+
return [] if gems.empty?
|
|
577
|
+
|
|
578
|
+
gem_home = gemset["gem_home"]
|
|
579
|
+
runtime_reason = gem_runtime_reason(gem_home)
|
|
580
|
+
provisioner = Molecules::GemsetProvisioner.new(runner: @runner)
|
|
581
|
+
gems.map do |gem|
|
|
582
|
+
if runtime_reason
|
|
583
|
+
next {"gem" => gem["name"], "version" => gem["version"], "gem_home" => gem_home,
|
|
584
|
+
"status" => FAIL, "reason" => runtime_reason}
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
kind, versions_or_message = provisioner.runtime_versions(
|
|
588
|
+
name: gem["name"], gem_home: gem_home
|
|
589
|
+
)
|
|
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
|
+
end
|
|
595
|
+
{"gem" => gem["name"], "version" => gem["version"], "gem_home" => gem_home,
|
|
596
|
+
"status" => status, "reason" => reason}
|
|
597
|
+
end
|
|
598
|
+
end
|
|
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
|
+
|
|
730
|
+
def gem_version_reason(gem, versions, gem_home)
|
|
731
|
+
return nil if versions.include?(gem["version"])
|
|
732
|
+
if versions.empty?
|
|
733
|
+
"gem '#{gem["name"]}' is not installed in gem runtime '#{gem_home}'; " \
|
|
734
|
+
"rerun ace-lab install"
|
|
735
|
+
else
|
|
736
|
+
"gem '#{gem["name"]}' is installed as #{versions.join(", ")} in " \
|
|
737
|
+
"'#{gem_home}' but the manifest declares #{gem["version"]}; rerun ace-lab install"
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def gem_runtime_reason(gem_home)
|
|
742
|
+
stat = lstat(gem_home)
|
|
743
|
+
if stat.nil?
|
|
744
|
+
"gem runtime '#{gem_home}' is missing; rerun ace-lab install"
|
|
745
|
+
elsif stat.symlink?
|
|
746
|
+
"gem runtime '#{gem_home}' is a symbolic link"
|
|
747
|
+
elsif !stat.directory?
|
|
748
|
+
"'#{gem_home}' exists but is not a directory"
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
|
|
554
752
|
# Resolve an executable without running it and judge it for the
|
|
555
753
|
# TARGET EXECUTORS (FINAL-LAB3): a name containing a separator
|
|
556
754
|
# must be a file executable at that path; a bare name must
|
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,8 +208,11 @@ 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
|
|
215
|
+
- lib/ace/lab/molecules/gemset_provisioner.rb
|
|
203
216
|
- lib/ace/lab/molecules/grant_provisioner.rb
|
|
204
217
|
- lib/ace/lab/molecules/grant_resolver.rb
|
|
205
218
|
- lib/ace/lab/molecules/hitl_routes.rb
|
|
@@ -208,6 +221,7 @@ files:
|
|
|
208
221
|
- lib/ace/lab/molecules/key_provisioner.rb
|
|
209
222
|
- lib/ace/lab/molecules/lab_state_scanner.rb
|
|
210
223
|
- lib/ace/lab/molecules/overseer_provisioner.rb
|
|
224
|
+
- lib/ace/lab/molecules/project_locations.rb
|
|
211
225
|
- lib/ace/lab/molecules/project_registry.rb
|
|
212
226
|
- lib/ace/lab/molecules/repository_provisioner.rb
|
|
213
227
|
- lib/ace/lab/molecules/role_verifier.rb
|