ace-lab 0.14.1 → 0.15.1
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 +12 -0
- data/docs/usage.md +78 -16
- data/lib/ace/lab/atoms/install_schema.rb +71 -5
- data/lib/ace/lab/auth/credential_handoff.rb +34 -10
- data/lib/ace/lab/auth/error.rb +13 -0
- data/lib/ace/lab/auth/lease_store.rb +91 -29
- data/lib/ace/lab/auth/local_command.rb +62 -0
- data/lib/ace/lab/auth/plugins/forgejo_provider.rb +123 -55
- data/lib/ace/lab/auth/profile_schema.rb +6 -1
- data/lib/ace/lab/auth/providers.rb +8 -0
- data/lib/ace/lab/auth/service.rb +350 -112
- data/lib/ace/lab/auth/session_configuration.rb +44 -0
- data/lib/ace/lab/cli/commands/prepare.rb +2 -1
- data/lib/ace/lab/cli/commands/verify.rb +2 -1
- data/lib/ace/lab/molecules/extension_projection.rb +60 -1
- data/lib/ace/lab/molecules/extension_provisioner.rb +21 -4
- data/lib/ace/lab/molecules/gem_payload_permissions.rb +12 -5
- data/lib/ace/lab/molecules/gemset_provisioner.rb +6 -1
- data/lib/ace/lab/organisms/install.rb +1 -0
- data/lib/ace/lab/organisms/verify.rb +56 -13
- data/lib/ace/lab/version.rb +1 -1
- data/libexec/forgejo-askpass +10 -0
- metadata +12 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error"
|
|
4
|
+
require_relative "../molecules/install_manifest_loader"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Lab
|
|
8
|
+
module Auth
|
|
9
|
+
# Passive launch data from the installed manifest. Roles select a brief
|
|
10
|
+
# and a project-specific environment, not an operating-system identity.
|
|
11
|
+
class SessionConfiguration
|
|
12
|
+
ROLES = %w[lab-overseer project-overseer worker verifier integrator].freeze
|
|
13
|
+
Selection = Data.define(:project, :path, :role, :instructions, :gem_home, :runtime, :environment_file)
|
|
14
|
+
|
|
15
|
+
def initialize(loader: -> { Molecules::InstallManifestLoader.load })
|
|
16
|
+
@loader = loader
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve(project:, role:)
|
|
20
|
+
document = @loader.call
|
|
21
|
+
raise Error, "session role is not supported" unless ROLES.include?(role)
|
|
22
|
+
|
|
23
|
+
scope = if role == "lab-overseer"
|
|
24
|
+
document["coordinator"] if document.dig("coordinator", "id") == project
|
|
25
|
+
else
|
|
26
|
+
document.fetch("projects").find { |entry| entry["id"] == project }
|
|
27
|
+
end
|
|
28
|
+
selected_role = document.fetch("roles").find { |entry| entry["id"] == role }
|
|
29
|
+
raise Error, "session project or role is not configured" unless scope && selected_role
|
|
30
|
+
|
|
31
|
+
runtime = document.fetch("session_runtime")
|
|
32
|
+
raise Error, "session Pi runtime is not configured" unless runtime.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
Selection.new(project: project, path: scope.fetch("path"), role: role,
|
|
35
|
+
instructions: selected_role.fetch("instructions"),
|
|
36
|
+
gem_home: document.fetch("gemset").fetch("gem_home"), runtime: runtime,
|
|
37
|
+
environment_file: scope.fetch("environment_files", {})[role])
|
|
38
|
+
rescue KeyError, NoMethodError
|
|
39
|
+
raise Error, "session deployment configuration is incomplete"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -19,7 +19,8 @@ module Ace
|
|
|
19
19
|
"directories" => ->(item) { item["path"] },
|
|
20
20
|
"keys" => ->(item) { "#{item['account']}:#{item['path']}" },
|
|
21
21
|
"roles" => ->(item) { item["id"] },
|
|
22
|
-
"gems" => ->(item) { "#{item['name']} #{item['version']}" }
|
|
22
|
+
"gems" => ->(item) { "#{item['name']} #{item['version']}" },
|
|
23
|
+
"extensions" => ->(item) { item["gem"] }
|
|
23
24
|
}.freeze
|
|
24
25
|
|
|
25
26
|
# "SHA256:" plus the first 20 base64 characters is enough to
|
|
@@ -16,7 +16,8 @@ module Ace
|
|
|
16
16
|
"accounts" => "account", "directories" => "directory", "keys" => "account",
|
|
17
17
|
"roles" => "id", "repositories" => "project", "grants" => "principal",
|
|
18
18
|
"registry" => "project", "overseer" => "project",
|
|
19
|
-
"adapters" => "adapter", "tools" => "tool", "gems" => "gem"
|
|
19
|
+
"adapters" => "adapter", "tools" => "tool", "gems" => "gem",
|
|
20
|
+
"extensions" => "gem"
|
|
20
21
|
}.freeze
|
|
21
22
|
|
|
22
23
|
desc "Verify the installed lab's readiness under the target accounts (read-only): " \
|
|
@@ -17,6 +17,59 @@ module Ace
|
|
|
17
17
|
|
|
18
18
|
module_function
|
|
19
19
|
|
|
20
|
+
# Installed operational extensions must not sit below a directory
|
|
21
|
+
# that an agent can replace. The leaf may be created by the root
|
|
22
|
+
# installer, but every existing ancestor must already be protected.
|
|
23
|
+
def validate_protected_root!(path)
|
|
24
|
+
unless path.is_a?(String) && path.start_with?("/") && File.expand_path(path) == path && path != "/"
|
|
25
|
+
raise Ace::Lab::Error, "installed extension path must be a canonical absolute path"
|
|
26
|
+
end
|
|
27
|
+
current = "/"
|
|
28
|
+
root_stat = File.lstat(current)
|
|
29
|
+
unless root_stat.directory? && root_stat.uid.zero? && (root_stat.mode & 0o022).zero?
|
|
30
|
+
raise Ace::Lab::Error, "installed extension filesystem root is not root-controlled"
|
|
31
|
+
end
|
|
32
|
+
path.split("/").reject(&:empty?).each do |part|
|
|
33
|
+
current = File.join(current, part)
|
|
34
|
+
begin
|
|
35
|
+
stat = File.lstat(current)
|
|
36
|
+
rescue Errno::ENOENT
|
|
37
|
+
break
|
|
38
|
+
end
|
|
39
|
+
unless stat.directory? && !stat.symlink? && stat.uid.zero? && (stat.mode & 0o022).zero?
|
|
40
|
+
raise Ace::Lab::Error, "installed extension directory is not root-controlled: #{current}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
rescue SystemCallError => e
|
|
45
|
+
raise Ace::Lab::Error, "installed extension directory cannot be inspected (#{e.class})"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate_protected_files!(root, relative_paths, allow_missing: false)
|
|
49
|
+
validate_protected_root!(root)
|
|
50
|
+
relative_paths.each do |relative|
|
|
51
|
+
validate_destination!(root, relative)
|
|
52
|
+
current = root
|
|
53
|
+
segments = relative.split(File::SEPARATOR)
|
|
54
|
+
segments.each_with_index do |part, index|
|
|
55
|
+
current = File.join(current, part)
|
|
56
|
+
begin
|
|
57
|
+
stat = File.lstat(current)
|
|
58
|
+
rescue Errno::ENOENT
|
|
59
|
+
break if allow_missing
|
|
60
|
+
raise
|
|
61
|
+
end
|
|
62
|
+
kind_ok = index == segments.length - 1 ? stat.file? : stat.directory?
|
|
63
|
+
unless kind_ok && !stat.symlink? && stat.uid.zero? && (stat.mode & 0o022).zero?
|
|
64
|
+
raise Ace::Lab::Error, "installed extension path is not root-controlled: #{current}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
true
|
|
69
|
+
rescue SystemCallError => e
|
|
70
|
+
raise Ace::Lab::Error, "installed extension file cannot be inspected (#{e.class})"
|
|
71
|
+
end
|
|
72
|
+
|
|
20
73
|
# The installed gem's own tree at the EXACT declared version.
|
|
21
74
|
# @return [String, nil] the handbook/extensions directory or nil
|
|
22
75
|
# when the gem (or its extensions tree) is not installed
|
|
@@ -30,10 +83,16 @@ module Ace
|
|
|
30
83
|
# gem's handbook/extensions tree (e.g. "ace-wake/index.js").
|
|
31
84
|
# @return [Hash<String, String>]
|
|
32
85
|
def source_files(source_root)
|
|
33
|
-
Dir.glob(File.join(source_root, "**", "*")).select { |path| File.file?(path) }
|
|
86
|
+
Dir.glob(File.join(source_root, "**", "*"), File::FNM_DOTMATCH).select { |path| File.file?(path) }
|
|
34
87
|
.to_h { |path| [path.delete_prefix("#{source_root}/"), path] }
|
|
35
88
|
end
|
|
36
89
|
|
|
90
|
+
def valid_receipt?(receipt)
|
|
91
|
+
receipt.is_a?(Hash) && receipt["files"].is_a?(Array) && receipt["files"].all? do |path|
|
|
92
|
+
path.is_a?(String) && !path.start_with?("/") && path.split("/", -1).all? { |segment| !segment.empty? && segment != "." && segment != ".." }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
37
96
|
# Resolve and fully validate a projection destination before
|
|
38
97
|
# anything is written or verified (the provider_syncer
|
|
39
98
|
# validate_projection_destination posture): every path
|
|
@@ -83,15 +83,28 @@ module Ace
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
output_dir = entry["path"]
|
|
86
|
+
if ENV["ACE_TOOLCHAIN"] == "installed"
|
|
87
|
+
begin
|
|
88
|
+
ExtensionProjection.validate_protected_files!(source, expected.keys)
|
|
89
|
+
ExtensionProjection.validate_protected_files!(output_dir,
|
|
90
|
+
expected.keys + [RECEIPT_NAME], allow_missing: true)
|
|
91
|
+
rescue Ace::Lab::Error => e
|
|
92
|
+
return failed(entry, version, e.message)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
86
95
|
escape = projection_escape_reason(output_dir, expected.keys)
|
|
87
96
|
return failed(entry, version, escape) if escape
|
|
88
97
|
|
|
89
98
|
receipt = read_receipt(output_dir)
|
|
99
|
+
if receipt
|
|
100
|
+
escape = projection_escape_reason(output_dir, receipt["files"])
|
|
101
|
+
return failed(entry, version, escape) if escape
|
|
102
|
+
end
|
|
90
103
|
loaded_version = receipt && receipt["gem"] == entry["gem"] ? receipt["version"] : nil
|
|
91
104
|
|
|
92
105
|
changed = projection_changed?(output_dir, expected, receipt, version)
|
|
93
106
|
if !changed
|
|
94
|
-
converge_modes(output_dir, expected, entry)
|
|
107
|
+
converge_modes(output_dir, expected, entry) unless @dry_run
|
|
95
108
|
return entry_report(entry, version, "unchanged", loaded_version, false,
|
|
96
109
|
receipt ? receipt.fetch("files") : expected.keys)
|
|
97
110
|
end
|
|
@@ -157,6 +170,7 @@ module Ace
|
|
|
157
170
|
# touched).
|
|
158
171
|
previous = read_receipt(output_dir)&.fetch("files", []) || []
|
|
159
172
|
retained = previous.filter_map do |relative|
|
|
173
|
+
ExtensionProjection.validate_destination!(output_dir, relative)
|
|
160
174
|
target = File.join(output_dir, relative)
|
|
161
175
|
next relative if expected.key?(relative)
|
|
162
176
|
|
|
@@ -175,22 +189,25 @@ module Ace
|
|
|
175
189
|
# so the target executor can always read its extensions.
|
|
176
190
|
def converge_modes(output_dir, expected, entry)
|
|
177
191
|
account = @account_lookup.call(entry["account"])
|
|
192
|
+
owner = ENV["ACE_TOOLCHAIN"] == "installed" ? [0, 0] : (account && [account.uid, account.gid])
|
|
178
193
|
targets = expected.keys.map { |relative| File.join(output_dir, relative) }
|
|
179
194
|
targets << File.join(output_dir, RECEIPT_NAME)
|
|
180
195
|
targets.each do |target|
|
|
181
196
|
next unless File.file?(target)
|
|
182
197
|
|
|
183
198
|
FileUtils.chmod(FILE_MODE, target)
|
|
184
|
-
FileUtils.chown(
|
|
199
|
+
FileUtils.chown(*owner, target) if owner
|
|
185
200
|
end
|
|
186
201
|
root = File.realpath(output_dir)
|
|
202
|
+
FileUtils.chmod(DIR_MODE, root)
|
|
203
|
+
FileUtils.chown(*owner, root) if owner
|
|
187
204
|
dirs = expected.keys.filter_map do |relative|
|
|
188
205
|
File.dirname(File.join(root, relative)) if relative.include?(File::SEPARATOR)
|
|
189
206
|
end.uniq
|
|
190
207
|
until dirs.empty?
|
|
191
208
|
dir = dirs.pop
|
|
192
209
|
FileUtils.chmod(DIR_MODE, dir)
|
|
193
|
-
FileUtils.chown(
|
|
210
|
+
FileUtils.chown(*owner, dir) if owner
|
|
194
211
|
parent = File.dirname(dir)
|
|
195
212
|
dirs << parent if dir != root && parent.start_with?("#{root}/") && !dirs.include?(parent) && parent != root
|
|
196
213
|
end
|
|
@@ -210,7 +227,7 @@ module Ace
|
|
|
210
227
|
return nil unless File.file?(path)
|
|
211
228
|
|
|
212
229
|
document = JSON.parse(File.read(path))
|
|
213
|
-
|
|
230
|
+
ExtensionProjection.valid_receipt?(document) ? document : nil
|
|
214
231
|
rescue JSON::ParserError, Errno::EACCES
|
|
215
232
|
nil
|
|
216
233
|
end
|
|
@@ -20,6 +20,7 @@ module Ace
|
|
|
20
20
|
Gem.use_paths(home)
|
|
21
21
|
queue = [Gem::Specification.find_by_name(ARGV.shift, ARGV.shift)]
|
|
22
22
|
dry_run = ARGV.shift == "dry-run"
|
|
23
|
+
require_complete = ARGV.shift == "complete"
|
|
23
24
|
seen = {}; rows = []
|
|
24
25
|
until queue.empty?
|
|
25
26
|
spec = queue.shift
|
|
@@ -47,28 +48,34 @@ module Ace
|
|
|
47
48
|
rescue Gem::MissingSpecError
|
|
48
49
|
# Convergence already predicts installing absent dependencies.
|
|
49
50
|
# Validate the existing payload; do not invent future files.
|
|
50
|
-
raise unless dry_run
|
|
51
|
+
raise unless dry_run && !require_complete
|
|
51
52
|
end
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
puts JSON.generate(rows)
|
|
55
56
|
CODE
|
|
56
57
|
|
|
57
|
-
def initialize(gem_home:, dry_run:, privileged:)
|
|
58
|
+
def initialize(gem_home:, dry_run:, privileged:, require_complete: false)
|
|
58
59
|
@declared_home = File.expand_path(gem_home)
|
|
59
60
|
raise Error, "gem runtime is a symlink" if File.symlink?(@declared_home)
|
|
60
61
|
@home = File.realpath(@declared_home)
|
|
61
62
|
@dry_run, @privileged = dry_run, privileged
|
|
63
|
+
@require_complete = require_complete
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def call(name:, version:)
|
|
65
67
|
# Default Ruby gems need no mutation in this runtime. With no local
|
|
66
68
|
# specifications there cannot be any owned installed payload here.
|
|
67
|
-
|
|
69
|
+
unless File.directory?(File.join(@home, "specifications"))
|
|
70
|
+
raise Error, "installed gem specifications are missing" if @require_complete
|
|
71
|
+
return []
|
|
72
|
+
end
|
|
68
73
|
|
|
69
74
|
env = ENV.keys.grep(/\ABUNDLE/).to_h { |key| [key, nil] }
|
|
70
|
-
env.merge!("RUBYOPT" => nil, "RUBYLIB" => nil, "
|
|
71
|
-
|
|
75
|
+
env.merge!("RUBYOPT" => nil, "RUBYLIB" => nil, "RUBYGEMS_GEMDEPS" => nil, "GEM_SPEC_CACHE" => nil,
|
|
76
|
+
"LD_PRELOAD" => nil, "LD_LIBRARY_PATH" => nil, "LD_AUDIT" => nil, "GEM_HOME" => @home, "GEM_PATH" => @home)
|
|
77
|
+
out, _err, status = Open3.capture3(env, RbConfig.ruby, "-e", INVENTORY, @home, name, version,
|
|
78
|
+
@dry_run ? "dry-run" : "apply", @require_complete ? "complete" : "partial")
|
|
72
79
|
unless status.success?
|
|
73
80
|
detail = begin
|
|
74
81
|
JSON.parse(out)["error"]
|
|
@@ -334,8 +334,13 @@ module Ace
|
|
|
334
334
|
|
|
335
335
|
def satisfying_versions(dep, versions)
|
|
336
336
|
requirement = dep["requirement"].split(",").map(&:strip)
|
|
337
|
+
constraint = Gem::Requirement.new(*requirement)
|
|
337
338
|
versions.select do |version|
|
|
338
|
-
|
|
339
|
+
# RubyGems can activate an explicitly installed prerelease even
|
|
340
|
+
# when the depending gem uses a normal version requirement.
|
|
341
|
+
# Dependency#match? filters prereleases intended for discovery;
|
|
342
|
+
# closure checks must describe what this installed runtime can use.
|
|
343
|
+
constraint.satisfied_by?(Gem::Version.new(version))
|
|
339
344
|
end
|
|
340
345
|
end
|
|
341
346
|
|
|
@@ -76,6 +76,7 @@ module Ace
|
|
|
76
76
|
effective_dry_run = dry_run.nil? ? self.dry_run : dry_run
|
|
77
77
|
manifest = Molecules::InstallManifestLoader.load(path: manifest_path, platform: platform)
|
|
78
78
|
declared_projects = manifest["projects"].map { |project| project["id"] }
|
|
79
|
+
declared_projects << manifest["coordinator"]["id"] if manifest["coordinator"]
|
|
79
80
|
|
|
80
81
|
conflict = grants_molecule.foreign_grants?(declared_projects)
|
|
81
82
|
if conflict
|
|
@@ -9,6 +9,7 @@ require_relative "../molecules/extension_provisioner"
|
|
|
9
9
|
require_relative "../molecules/hitl_routes"
|
|
10
10
|
require_relative "../molecules/caller_authorizer"
|
|
11
11
|
require_relative "../atoms/topology_schema"
|
|
12
|
+
require "ace/support/cli/toolchain"
|
|
12
13
|
require "open3"
|
|
13
14
|
require "yaml"
|
|
14
15
|
require "json"
|
|
@@ -199,9 +200,8 @@ module Ace
|
|
|
199
200
|
# verified readiness. Protocol instructions are resolved with
|
|
200
201
|
# ace-nav from every declared project root (the execution
|
|
201
202
|
# contexts the executor works in); a role that fails resolution
|
|
202
|
-
# is a failed check
|
|
203
|
-
#
|
|
204
|
-
# remotes) and does not block.
|
|
203
|
+
# is a failed check. In auto mode a missing resolver reports unknown;
|
|
204
|
+
# installed mode requires the installed executable and fails closed.
|
|
205
205
|
def check_roles(manifest)
|
|
206
206
|
verifier = Molecules::RoleVerifier.new(
|
|
207
207
|
account_lookup: @account_lookup, group_source: @group_source
|
|
@@ -252,6 +252,8 @@ module Ace
|
|
|
252
252
|
"'#{instructions}'; not probed by the read-only verifier"] if unresolved_resolver
|
|
253
253
|
|
|
254
254
|
[PASS, nil]
|
|
255
|
+
rescue Ace::Support::Cli::Toolchain::Error => e
|
|
256
|
+
[FAIL, "ACE toolchain error: #{e.message}"]
|
|
255
257
|
end
|
|
256
258
|
|
|
257
259
|
def resolution_error(result)
|
|
@@ -259,20 +261,18 @@ module Ace
|
|
|
259
261
|
detail ? detail : "ace-nav exited with status #{result['status']}"
|
|
260
262
|
end
|
|
261
263
|
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
# project's binstub when present, otherwise the PATH lookup.
|
|
265
|
-
# A privileged installer never executes repository-controlled
|
|
266
|
-
# binstubs (same trust posture as the LAB-1 symlink refusals):
|
|
267
|
-
# under root only the PATH-installed resolver is used.
|
|
264
|
+
# Match the executor's resolver: installed mode always resolves the
|
|
265
|
+
# installed launcher, and root never executes repository binstubs.
|
|
268
266
|
def nav_binary(project_root)
|
|
267
|
+
return Ace::Support::Cli::Toolchain.executable("ace-nav") if Ace::Support::Cli::Toolchain.installed?
|
|
268
|
+
|
|
269
269
|
binstub = File.join(project_root, "bin", "ace-nav")
|
|
270
270
|
use_binstub = !Process.uid.zero? && File.executable?(binstub)
|
|
271
271
|
use_binstub ? binstub : "ace-nav"
|
|
272
272
|
end
|
|
273
273
|
|
|
274
274
|
def run_nav(argv, cwd)
|
|
275
|
-
out, err, status = Open3.capture3(*argv, chdir: cwd)
|
|
275
|
+
out, err, status = Open3.capture3(Ace::Support::Cli::Toolchain.child_environment, *argv, chdir: cwd)
|
|
276
276
|
{"status" => status.success? ? "ok" : (status.exitstatus || "signal #{status.termsig}"),
|
|
277
277
|
"out" => out, "err" => err}
|
|
278
278
|
rescue Errno::ENOENT
|
|
@@ -577,7 +577,12 @@ module Ace
|
|
|
577
577
|
|
|
578
578
|
gem_home = gemset["gem_home"]
|
|
579
579
|
runtime_reason = gem_runtime_reason(gem_home)
|
|
580
|
-
|
|
580
|
+
# All probes below are read-only and overlap heavily across root
|
|
581
|
+
# dependency graphs. Reuse identical command results only within
|
|
582
|
+
# this verification pass; a later call always reads the runtime again.
|
|
583
|
+
probes = {}
|
|
584
|
+
read_once = ->(argv) { probes.fetch(argv) { probes[argv] = @runner.call(argv) } }
|
|
585
|
+
provisioner = Molecules::GemsetProvisioner.new(runner: read_once)
|
|
581
586
|
gems.map do |gem|
|
|
582
587
|
if runtime_reason
|
|
583
588
|
next {"gem" => gem["name"], "version" => gem["version"], "gem_home" => gem_home,
|
|
@@ -657,8 +662,17 @@ module Ace
|
|
|
657
662
|
gem_home, entry["gem"], declared["version"]
|
|
658
663
|
)
|
|
659
664
|
return "gem '#{entry["gem"]}' #{declared["version"]} is not installed with a " \
|
|
660
|
-
"handbook/extensions tree in '#{gem_home}'; rerun ace-lab install"
|
|
661
|
-
|
|
665
|
+
"handbook/extensions tree in '#{gem_home}'; rerun ace-lab install" if source_root.nil?
|
|
666
|
+
|
|
667
|
+
if ENV["ACE_TOOLCHAIN"] == "installed"
|
|
668
|
+
begin
|
|
669
|
+
Molecules::ExtensionProjection.validate_protected_root!(source_root)
|
|
670
|
+
Molecules::ExtensionProjection.validate_protected_files!(entry["path"],
|
|
671
|
+
[Molecules::ExtensionProjection::RECEIPT_NAME])
|
|
672
|
+
rescue Ace::Lab::Error => e
|
|
673
|
+
return "#{e.message}; rerun ace-lab install"
|
|
674
|
+
end
|
|
675
|
+
end
|
|
662
676
|
receipt_path = File.join(entry["path"], Molecules::ExtensionProvisioner::RECEIPT_NAME)
|
|
663
677
|
unless File.file?(receipt_path)
|
|
664
678
|
return "extension projection receipt '#{receipt_path}' is missing; rerun ace-lab install"
|
|
@@ -672,6 +686,9 @@ module Ace
|
|
|
672
686
|
return "extension projection receipt '#{receipt_path}' is not readable; " \
|
|
673
687
|
"rerun ace-lab install"
|
|
674
688
|
end
|
|
689
|
+
unless Molecules::ExtensionProjection.valid_receipt?(receipt)
|
|
690
|
+
return "extension projection receipt '#{receipt_path}' has invalid files; rerun ace-lab install"
|
|
691
|
+
end
|
|
675
692
|
if receipt["gem"] != entry["gem"] || receipt["version"] != declared["version"]
|
|
676
693
|
return "extension projection at '#{entry["path"]}' was projected from " \
|
|
677
694
|
"'#{receipt["gem"]} #{receipt["version"]}' but the manifest declares " \
|
|
@@ -692,6 +709,11 @@ module Ace
|
|
|
692
709
|
expected.each_key do |relative|
|
|
693
710
|
Molecules::ExtensionProjection.validate_destination!(entry["path"], relative)
|
|
694
711
|
end
|
|
712
|
+
if ENV["ACE_TOOLCHAIN"] == "installed"
|
|
713
|
+
Molecules::ExtensionProjection.validate_protected_files!(source_root, expected.keys)
|
|
714
|
+
Molecules::ExtensionProjection.validate_protected_files!(entry["path"],
|
|
715
|
+
expected.keys + [Molecules::ExtensionProjection::RECEIPT_NAME])
|
|
716
|
+
end
|
|
695
717
|
rescue Ace::Lab::Error => e
|
|
696
718
|
return "#{e.message}; rerun ace-lab install"
|
|
697
719
|
end
|
|
@@ -713,7 +735,28 @@ module Ace
|
|
|
713
735
|
return "extension projection file '#{unreadable}' is not readable by the declared " \
|
|
714
736
|
"account '#{entry["account"]}'; rerun ace-lab install" if unreadable
|
|
715
737
|
|
|
738
|
+
directories = [entry["path"]] + expected.keys.flat_map do |relative|
|
|
739
|
+
segments = relative.split("/")[0...-1]
|
|
740
|
+
segments.each_index.map { |i| File.join(entry["path"], *segments[0..i]) }
|
|
741
|
+
end
|
|
742
|
+
if ENV["ACE_TOOLCHAIN"] == "installed"
|
|
743
|
+
parent = File.dirname(entry["path"])
|
|
744
|
+
loop do
|
|
745
|
+
directories << parent
|
|
746
|
+
break if parent == "/"
|
|
747
|
+
parent = File.dirname(parent)
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
inaccessible = directories.uniq.find do |dir|
|
|
751
|
+
stat = lstat(dir)
|
|
752
|
+
!stat || !stat.directory? || stat.symlink? ||
|
|
753
|
+
((stat.mode & 0o001).zero? && !((stat.mode & 0o100) != 0 && account_entry && stat.uid == account_entry.uid))
|
|
754
|
+
end
|
|
755
|
+
return "extension projection directory '#{inaccessible}' is not traversable by the declared account '#{entry["account"]}'" if inaccessible
|
|
756
|
+
|
|
716
757
|
nil
|
|
758
|
+
rescue SystemCallError => e
|
|
759
|
+
"extension projection at '#{entry["path"]}' cannot be inspected (#{e.class.name}); rerun ace-lab install"
|
|
717
760
|
end
|
|
718
761
|
|
|
719
762
|
def unreadable_reason(path, account_entry)
|
data/lib/ace/lab/version.rb
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Executable code belongs to the protected installed package, never the
|
|
3
|
+
# ephemeral credential directory (which may be mounted noexec).
|
|
4
|
+
if [ "$1" = "Username for '$ACE_LAB_FORGEJO_ORIGIN': " ]; then
|
|
5
|
+
printf '%s\n' "$ACE_LAB_FORGEJO_ACCOUNT"
|
|
6
|
+
elif [ "$1" = "Password for '$ACE_LAB_FORGEJO_SCHEME://$ACE_LAB_FORGEJO_ACCOUNT@$ACE_LAB_FORGEJO_AUTHORITY': " ]; then
|
|
7
|
+
cat "$ACE_LAB_CREDENTIAL_FILE"
|
|
8
|
+
else
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
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.15.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-11-
|
|
10
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -58,6 +58,9 @@ dependencies:
|
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
60
|
version: '0.7'
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 0.9.0
|
|
61
64
|
type: :runtime
|
|
62
65
|
prerelease: false
|
|
63
66
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +68,9 @@ dependencies:
|
|
|
65
68
|
- - "~>"
|
|
66
69
|
- !ruby/object:Gem::Version
|
|
67
70
|
version: '0.7'
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 0.9.0
|
|
68
74
|
- !ruby/object:Gem::Dependency
|
|
69
75
|
name: ace-support-test-helpers
|
|
70
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -175,13 +181,16 @@ files:
|
|
|
175
181
|
- lib/ace/lab/atoms/topology_schema.rb
|
|
176
182
|
- lib/ace/lab/auth.rb
|
|
177
183
|
- lib/ace/lab/auth/credential_handoff.rb
|
|
184
|
+
- lib/ace/lab/auth/error.rb
|
|
178
185
|
- lib/ace/lab/auth/lease_store.rb
|
|
186
|
+
- lib/ace/lab/auth/local_command.rb
|
|
179
187
|
- lib/ace/lab/auth/plugins/env_provider.rb
|
|
180
188
|
- lib/ace/lab/auth/plugins/forgejo_provider.rb
|
|
181
189
|
- lib/ace/lab/auth/plugins/github_provider.rb
|
|
182
190
|
- lib/ace/lab/auth/profile_schema.rb
|
|
183
191
|
- lib/ace/lab/auth/providers.rb
|
|
184
192
|
- lib/ace/lab/auth/service.rb
|
|
193
|
+
- lib/ace/lab/auth/session_configuration.rb
|
|
185
194
|
- lib/ace/lab/authorization_path.rb
|
|
186
195
|
- lib/ace/lab/cli.rb
|
|
187
196
|
- lib/ace/lab/cli/commands/agents.rb
|
|
@@ -234,6 +243,7 @@ files:
|
|
|
234
243
|
- lib/ace/lab/organisms/topology_service.rb
|
|
235
244
|
- lib/ace/lab/organisms/verify.rb
|
|
236
245
|
- lib/ace/lab/version.rb
|
|
246
|
+
- libexec/forgejo-askpass
|
|
237
247
|
homepage: https://github.com/cs3b/ace
|
|
238
248
|
licenses:
|
|
239
249
|
- MIT
|