ace-lab 0.9.0 → 0.13.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 +363 -104
- data/README.md +66 -100
- data/docs/usage.md +529 -592
- data/lib/ace/lab/atoms/install_schema.rb +373 -0
- data/lib/ace/lab/cli/commands/install.rb +161 -0
- data/lib/ace/lab/cli/commands/prepare.rb +118 -0
- data/lib/ace/lab/cli/commands/project_add.rb +101 -0
- data/lib/ace/lab/cli/commands/status.rb +109 -0
- data/lib/ace/lab/cli/commands/verify.rb +115 -0
- data/lib/ace/lab/cli.rb +21 -23
- data/lib/ace/lab/install_manifest_path.rb +23 -0
- data/lib/ace/lab/models/status_report.rb +38 -0
- data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
- data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
- data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
- data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
- data/lib/ace/lab/molecules/grant_resolver.rb +34 -23
- data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
- data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
- data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
- data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
- data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
- data/lib/ace/lab/molecules/project_registry.rb +251 -0
- data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
- data/lib/ace/lab/molecules/role_verifier.rb +223 -0
- data/lib/ace/lab/organisms/install.rb +213 -0
- data/lib/ace/lab/organisms/prepare.rb +156 -0
- data/lib/ace/lab/organisms/project_add.rb +170 -0
- data/lib/ace/lab/organisms/status_service.rb +70 -0
- data/lib/ace/lab/organisms/verify.rb +768 -0
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +34 -6
- metadata +34 -108
- data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
- data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
- data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
- data/lib/ace/lab/atoms/service_input.rb +0 -92
- data/lib/ace/lab/cli/commands/assignment.rb +0 -92
- data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
- data/lib/ace/lab/cli/commands/run.rb +0 -65
- data/lib/ace/lab/cli/commands/service.rb +0 -240
- data/lib/ace/lab/models/service_receipt.rb +0 -18
- data/lib/ace/lab/molecules/command_owner_admission.rb +0 -183
- data/lib/ace/lab/molecules/command_policy.rb +0 -158
- data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
- data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
- data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
- data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
- data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
- data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -152
- data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
- data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
- data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
- data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
- data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
- data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
- data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
- data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
- data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
- data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
- data/lib/ace/lab/molecules/service_executor.rb +0 -99
- data/lib/ace/lab/molecules/service_policy.rb +0 -289
- data/lib/ace/lab/organisms/authority_composition.rb +0 -87
- data/lib/ace/lab/organisms/command_executor.rb +0 -220
- data/lib/ace/lab/organisms/command_run_client.rb +0 -131
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
- data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
- data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
- data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
- data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
- data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
- data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
- data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
- data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
- data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
- data/lib/ace/lab/organisms/service_request_service.rb +0 -249
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "../molecules/repository_provisioner"
|
|
5
|
+
require_relative "../molecules/overseer_provisioner"
|
|
6
|
+
require_relative "../molecules/project_registry"
|
|
7
|
+
require_relative "../molecules/hitl_routes"
|
|
8
|
+
require "etc"
|
|
9
|
+
|
|
10
|
+
module Ace
|
|
11
|
+
module Lab
|
|
12
|
+
module Organisms
|
|
13
|
+
# Orchestrates `ace-lab project add` (L8.2, task 8xg.t.sbz.7.1):
|
|
14
|
+
# making a project ready for team work means (1) the repository
|
|
15
|
+
# exists at the declared path with the declared origin remote — the
|
|
16
|
+
# planner provisions task worktrees from it, (2) the private
|
|
17
|
+
# untracked `.ace-tasks/_overseer/` folder is prepared, and (3) the
|
|
18
|
+
# project is registered in the project's own `.ace/lab/config.yml`
|
|
19
|
+
# so `ace-lab status`, `ace-lab projects` and `ace-overseer
|
|
20
|
+
# projects` see it through the ordinary config cascade. Accounts,
|
|
21
|
+
# directories, keys and roles are machine-level facts already
|
|
22
|
+
# converged by `ace-lab prepare` (L8.1); this organism adds no
|
|
23
|
+
# other state owner. Steps run in a fixed order; a failed
|
|
24
|
+
# repository step fails dependents instead of attempting them.
|
|
25
|
+
# Re-adding an already-registered project re-validates and reports
|
|
26
|
+
# without rewriting; contradictory or foreign registrations are
|
|
27
|
+
# refused before anything is overwritten.
|
|
28
|
+
class ProjectAdd
|
|
29
|
+
attr_reader :dry_run
|
|
30
|
+
|
|
31
|
+
def initialize(runner: nil, account_lookup: nil, chown: nil, dry_run: false)
|
|
32
|
+
@runner = runner
|
|
33
|
+
# Production resolves declared owners through the REAL system
|
|
34
|
+
# passwd (same seam as RoleVerifier/Verify); only tests inject
|
|
35
|
+
# a lookup (PR #393 final audit, FINAL-LAB1: a nil default
|
|
36
|
+
# classified every declared owner as nonexistent).
|
|
37
|
+
@account_lookup = account_lookup || method(:lookup_passwd)
|
|
38
|
+
@chown = chown
|
|
39
|
+
@dry_run = dry_run
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param id [String] project id (topology id)
|
|
43
|
+
# @param path [String] absolute project path
|
|
44
|
+
# @param remote [String, nil] origin remote URL
|
|
45
|
+
# @param label [String, nil] display label
|
|
46
|
+
# @param owner [String, nil] target executor account the
|
|
47
|
+
# project tree must belong to when install runs privileged
|
|
48
|
+
# (root); nil leaves ownership untouched
|
|
49
|
+
# @param hitl_adapters [Array<Hash>] normalized manifest
|
|
50
|
+
# adapters; the `kind: hitl` declarations are projected into
|
|
51
|
+
# the project's runtime HITL route configuration (FINAL-LAB2)
|
|
52
|
+
# @param dry_run [Boolean, nil] per-call override
|
|
53
|
+
# @return [Hash] report: ok / dry_run / project / repository /
|
|
54
|
+
# overseer / registry / hitl / owner
|
|
55
|
+
def call(id:, path:, remote: nil, label: nil, owner: nil, hitl_adapters: [], dry_run: nil)
|
|
56
|
+
effective_dry_run = dry_run.nil? ? self.dry_run : dry_run
|
|
57
|
+
repository = Molecules::RepositoryProvisioner.new(runner: @runner, dry_run: effective_dry_run)
|
|
58
|
+
.call(path: path, remote: remote)
|
|
59
|
+
ready = repository["status"] != "failed"
|
|
60
|
+
|
|
61
|
+
overseer = if ready
|
|
62
|
+
Molecules::OverseerProvisioner.new(runner: @runner, dry_run: effective_dry_run).call(root: path)
|
|
63
|
+
else
|
|
64
|
+
skipped("_overseer", "repository not ready; fix the repository step first")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
registry = if ready
|
|
68
|
+
Molecules::ProjectRegistry.new.register(
|
|
69
|
+
root: path,
|
|
70
|
+
project: {"id" => id, "label" => label, "path" => path, "remote" => remote},
|
|
71
|
+
dry_run: effective_dry_run
|
|
72
|
+
)
|
|
73
|
+
else
|
|
74
|
+
skipped(".ace/lab/config.yml", "repository not ready; fix the repository step first")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
converged = [repository, overseer, registry].none? { |item| item["status"] == "failed" }
|
|
78
|
+
hitl = if converged
|
|
79
|
+
Molecules::HitlRoutes.new.install(
|
|
80
|
+
root: path, adapters: hitl_adapters, dry_run: effective_dry_run
|
|
81
|
+
)
|
|
82
|
+
else
|
|
83
|
+
skipped(".ace/hitl/config.yml", "repository not ready; fix the repository step first")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
steps = {"repository" => repository, "overseer" => overseer,
|
|
87
|
+
"registry" => registry, "hitl" => hitl}
|
|
88
|
+
ownership = if steps.any? { |_step, item| item["status"] == "failed" }
|
|
89
|
+
failed_step, = steps.find { |_step, item| item["status"] == "failed" }
|
|
90
|
+
skipped_owner(owner, "step '#{failed_step}' is not ready; fix that step first")
|
|
91
|
+
else
|
|
92
|
+
propagate_owner(path, owner, effective_dry_run)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
"ok" => [repository, overseer, registry, hitl, ownership].none? { |item| item["status"] == "failed" },
|
|
97
|
+
"dry_run" => effective_dry_run,
|
|
98
|
+
"project" => id,
|
|
99
|
+
"path" => path,
|
|
100
|
+
"remote" => remote,
|
|
101
|
+
"repository" => repository,
|
|
102
|
+
"overseer" => overseer,
|
|
103
|
+
"registry" => registry,
|
|
104
|
+
"hitl" => hitl,
|
|
105
|
+
"owner" => ownership
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# Root-installed projects must end up owned by the declared
|
|
112
|
+
# target executor, or the executor cannot work in them (LAB-2).
|
|
113
|
+
# Chown is a privileged operation: a non-root installer reports
|
|
114
|
+
# skipped (verify then judges executor access on the real bits).
|
|
115
|
+
def propagate_owner(path, owner, dry_run)
|
|
116
|
+
base = {"account" => owner, "path" => path}
|
|
117
|
+
return base.merge("status" => "skipped", "message" => nil) if owner.nil?
|
|
118
|
+
|
|
119
|
+
entry = @account_lookup ? @account_lookup.call(owner) : nil
|
|
120
|
+
if entry.nil?
|
|
121
|
+
return base.merge("status" => "failed",
|
|
122
|
+
"message" => "owner account '#{owner}' does not exist; fix the account phase first")
|
|
123
|
+
end
|
|
124
|
+
return base.merge("status" => "would_propagate", "message" => nil) if dry_run
|
|
125
|
+
unless Process.uid.zero?
|
|
126
|
+
return base.merge("status" => "skipped",
|
|
127
|
+
"message" => "installer is not root; ownership left untouched (verify reports " \
|
|
128
|
+
"the executor's actual access)")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A root uid match on the project root proves nothing about
|
|
132
|
+
# generated children (root may have just created
|
|
133
|
+
# `.ace/lab/config.yml` / `.ace/hitl/config.yml` mode 0600
|
|
134
|
+
# with its own uid inside), so a root installer ALWAYS
|
|
135
|
+
# converges the whole tree instead of skipping.
|
|
136
|
+
begin
|
|
137
|
+
chown.call(entry.uid, entry.gid, path)
|
|
138
|
+
rescue SystemCallError, ArgumentError, TypeError => e
|
|
139
|
+
return base.merge("status" => "failed",
|
|
140
|
+
"message" => "propagating ownership of '#{path}' to '#{owner}' failed: #{e.message}")
|
|
141
|
+
end
|
|
142
|
+
base.merge("status" => "propagated", "message" => nil)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Privileged recursive chown; injectable so tests can exercise
|
|
146
|
+
# the root branch without uid 0.
|
|
147
|
+
def chown
|
|
148
|
+
@chown || ->(uid, gid, path) {
|
|
149
|
+
require "fileutils"
|
|
150
|
+
FileUtils.chown_R(uid, gid, path)
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def skipped_owner(owner, message)
|
|
155
|
+
{"account" => owner, "path" => nil, "status" => "failed", "message" => message}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def skipped(location, message)
|
|
159
|
+
{"location" => location, "status" => "failed", "message" => message}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def lookup_passwd(username)
|
|
163
|
+
Etc.getpwnam(username)
|
|
164
|
+
rescue ArgumentError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Lab
|
|
7
|
+
module Organisms
|
|
8
|
+
# Assembles the lab status report: configuration classification plus
|
|
9
|
+
# the read-only state scan. Strictly read-only observability — no
|
|
10
|
+
# authorization gate (the fail-closed CallerAuthorizer path belongs
|
|
11
|
+
# to the topology query commands), no writes, no services started,
|
|
12
|
+
# no installer dependency (task 8xg.t.sbz.6.0).
|
|
13
|
+
class StatusService
|
|
14
|
+
def initialize(classifier: nil, runtime_reader: nil, scanner: nil, root: nil)
|
|
15
|
+
@classifier = classifier || Molecules::ConfigStateClassifier
|
|
16
|
+
@runtime_reader = runtime_reader || Molecules::ConfigStateClassifier
|
|
17
|
+
@scanner = scanner || Molecules::LabStateScanner
|
|
18
|
+
@root = root || resolve_project_root
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Production factory: a service that resolves the cascade on each
|
|
22
|
+
# call, so every report observes current files. A broken cascade
|
|
23
|
+
# becomes the report's :invalid section instead of an exception.
|
|
24
|
+
def self.from_config(root: nil)
|
|
25
|
+
new(root: root)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Models::StatusReport]
|
|
29
|
+
def report
|
|
30
|
+
documents, cascade_error = begin
|
|
31
|
+
[Ace::Lab.cascade_documents, nil]
|
|
32
|
+
rescue Ace::Lab::InvalidConfigurationError => e
|
|
33
|
+
[nil, e.message]
|
|
34
|
+
end
|
|
35
|
+
build(documents: documents, cascade_error: cascade_error)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param documents [Array<Hash>, nil]
|
|
39
|
+
# @param cascade_error [String, nil]
|
|
40
|
+
# @return [Models::StatusReport]
|
|
41
|
+
def build(documents:, cascade_error: nil)
|
|
42
|
+
config = @classifier.classify(documents: documents, cascade_error: cascade_error, root: @root)
|
|
43
|
+
runtime = @runtime_reader.runtime_config(root: @root)
|
|
44
|
+
Models::StatusReport.new(
|
|
45
|
+
config: config,
|
|
46
|
+
runtime_config: runtime,
|
|
47
|
+
state: @scanner.scan(root: @root),
|
|
48
|
+
root: @root
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# State locations belong to the project root, not the caller's cwd:
|
|
55
|
+
# running from a subdirectory must not misreport an initialized lab
|
|
56
|
+
# as absent. Same resolution as ace-coworker's ProjectRoot (git
|
|
57
|
+
# worktree top), falling back to the cwd outside a repository.
|
|
58
|
+
def resolve_project_root
|
|
59
|
+
out, _err, status = Open3.capture3("git", "rev-parse", "--show-toplevel")
|
|
60
|
+
return Dir.pwd unless status.success?
|
|
61
|
+
|
|
62
|
+
root = out.to_s.strip
|
|
63
|
+
root.empty? ? Dir.pwd : root
|
|
64
|
+
rescue StandardError
|
|
65
|
+
Dir.pwd
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|