ace-lab 0.9.0 → 0.13.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +399 -102
  3. data/README.md +66 -100
  4. data/docs/usage.md +563 -592
  5. data/lib/ace/lab/atoms/install_schema.rb +444 -0
  6. data/lib/ace/lab/cli/commands/install.rb +161 -0
  7. data/lib/ace/lab/cli/commands/prepare.rb +120 -0
  8. data/lib/ace/lab/cli/commands/project_add.rb +101 -0
  9. data/lib/ace/lab/cli/commands/status.rb +109 -0
  10. data/lib/ace/lab/cli/commands/verify.rb +115 -0
  11. data/lib/ace/lab/cli.rb +21 -23
  12. data/lib/ace/lab/install_manifest_path.rb +23 -0
  13. data/lib/ace/lab/models/status_report.rb +38 -0
  14. data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
  15. data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
  16. data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
  17. data/lib/ace/lab/molecules/gemset_provisioner.rb +222 -0
  18. data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
  19. data/lib/ace/lab/molecules/grant_resolver.rb +34 -23
  20. data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
  21. data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
  22. data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
  23. data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
  24. data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
  25. data/lib/ace/lab/molecules/project_registry.rb +251 -0
  26. data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
  27. data/lib/ace/lab/molecules/role_verifier.rb +223 -0
  28. data/lib/ace/lab/organisms/install.rb +218 -0
  29. data/lib/ace/lab/organisms/prepare.rb +170 -0
  30. data/lib/ace/lab/organisms/project_add.rb +170 -0
  31. data/lib/ace/lab/organisms/status_service.rb +70 -0
  32. data/lib/ace/lab/organisms/verify.rb +836 -0
  33. data/lib/ace/lab/version.rb +1 -1
  34. data/lib/ace/lab.rb +34 -6
  35. metadata +35 -108
  36. data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
  37. data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
  38. data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
  39. data/lib/ace/lab/atoms/service_input.rb +0 -92
  40. data/lib/ace/lab/cli/commands/assignment.rb +0 -92
  41. data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
  42. data/lib/ace/lab/cli/commands/run.rb +0 -65
  43. data/lib/ace/lab/cli/commands/service.rb +0 -240
  44. data/lib/ace/lab/models/service_receipt.rb +0 -18
  45. data/lib/ace/lab/molecules/command_owner_admission.rb +0 -183
  46. data/lib/ace/lab/molecules/command_policy.rb +0 -158
  47. data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
  48. data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
  49. data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
  50. data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
  51. data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
  52. data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -152
  53. data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
  54. data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
  55. data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
  56. data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
  57. data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
  58. data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
  59. data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
  60. data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
  61. data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
  62. data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
  63. data/lib/ace/lab/molecules/service_executor.rb +0 -99
  64. data/lib/ace/lab/molecules/service_policy.rb +0 -289
  65. data/lib/ace/lab/organisms/authority_composition.rb +0 -87
  66. data/lib/ace/lab/organisms/command_executor.rb +0 -220
  67. data/lib/ace/lab/organisms/command_run_client.rb +0 -131
  68. data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
  69. data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
  70. data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
  71. data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
  72. data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
  73. data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
  74. data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
  75. data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
  76. data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
  77. data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
  78. data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
  79. data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
  80. data/lib/ace/lab/organisms/service_request_service.rb +0 -249
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "ace/support/cli"
5
+ require "ace/core"
6
+ require_relative "../../organisms/prepare"
7
+
8
+ module Ace
9
+ module Lab
10
+ module CLI
11
+ module Commands
12
+ class Prepare < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ # One shared section-namer map: the table renderer and the
16
+ # failure summary must name items identically.
17
+ SECTION_NAMERS = {
18
+ "accounts" => ->(item) { item["username"] },
19
+ "directories" => ->(item) { item["path"] },
20
+ "keys" => ->(item) { "#{item['account']}:#{item['path']}" },
21
+ "roles" => ->(item) { item["id"] },
22
+ "gems" => ->(item) { "#{item['name']} #{item['version']}" }
23
+ }.freeze
24
+
25
+ # "SHA256:" plus the first 20 base64 characters is enough to
26
+ # eyeball-match a key without ballooning the table row.
27
+ FINGERPRINT_WIDTH = 27
28
+
29
+ desc "Prepare accounts, directories, permissions, keys, roles and the declared " \
30
+ "gem set from the trusted install manifest"
31
+
32
+ example [
33
+ "--dry-run # Predict all sections without mutating",
34
+ "--format json # Machine-readable report"
35
+ ]
36
+
37
+ option :dry_run, type: :boolean, desc: "Predict changes without applying them"
38
+ option :format, type: :string, desc: "Output format (table, json; default: table)"
39
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
40
+
41
+ def initialize(prepare: nil)
42
+ super()
43
+ @prepare = prepare
44
+ end
45
+
46
+ def call(**options)
47
+ format = options[:format] || "table"
48
+ unless %w[table json].include?(format)
49
+ raise Ace::Support::Cli::Error,
50
+ "--format \"#{format}\" is not supported; table or json"
51
+ end
52
+
53
+ organism = @prepare || Organisms::Prepare.new
54
+ report = organism.call(dry_run: options[:dry_run] || false)
55
+ render(report, format: format, quiet: options[:quiet])
56
+
57
+ # A wet run with failed items never exits 0: the report renders
58
+ # first, then the failure list names each item explicitly.
59
+ # Dry-run conflicts are predicted failures, not execution
60
+ # failures — exit 0 so callers can script a preview.
61
+ unless report["ok"] || report["dry_run"]
62
+ raise Ace::Support::Cli::Error, failure_summary(report)
63
+ end
64
+ rescue Ace::Lab::InvalidConfigurationError => e
65
+ raise Ace::Support::Cli::Error, e.message
66
+ end
67
+
68
+ private
69
+
70
+ def render(report, format:, quiet:)
71
+ return if quiet
72
+
73
+ puts format == "json" ? JSON.generate(report) : render_table(report)
74
+ end
75
+
76
+ def render_table(report)
77
+ lines = ["Machine: #{report['machine']} manifest: #{report['manifest']}" \
78
+ "#{" (dry run)" if report['dry_run']}"]
79
+ SECTION_NAMERS.each do |section, namer|
80
+ lines << "#{section.capitalize}:"
81
+ items = report[section]
82
+ if items.empty?
83
+ lines << " (none declared)"
84
+ else
85
+ items.each do |item|
86
+ detail = detail_for(section, item)
87
+ lines << " #{namer.call(item).to_s.ljust(34)} #{item['status']}#{detail}"
88
+ end
89
+ end
90
+ end
91
+ lines << "Result: #{report['ok'] ? 'ok' : 'FAILED'}"
92
+ lines.join("\n")
93
+ end
94
+
95
+ # Key rows render a truncated public fingerprint (never key
96
+ # material); everything else appends the message when present.
97
+ def detail_for(section, item)
98
+ parts = []
99
+ if section == "keys" && item["fingerprint"]
100
+ fingerprint = item["fingerprint"]
101
+ parts << (fingerprint.length <= FINGERPRINT_WIDTH ? fingerprint :
102
+ "#{fingerprint[0, FINGERPRINT_WIDTH]}…")
103
+ end
104
+ parts << item["message"] if item["message"]
105
+ parts.empty? ? "" : " #{parts.join(' ')}"
106
+ end
107
+
108
+ def failure_summary(report)
109
+ failures = SECTION_NAMERS.flat_map do |section, namer|
110
+ report[section]
111
+ .select { |item| item["status"] == "failed" }
112
+ .map { |item| "#{section}/#{namer.call(item)}: #{item['message']}" }
113
+ end
114
+ "prepare failed (#{failures.size} item(s)): #{failures.join('; ')}"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "ace/support/cli"
5
+ require "ace/core"
6
+ require_relative "../../organisms/project_add"
7
+
8
+ module Ace
9
+ module Lab
10
+ module CLI
11
+ module Commands
12
+ class ProjectAdd < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ SECTION_NAMERS = {
16
+ "repository" => ->(item) { item["path"] || item["location"] },
17
+ "overseer" => ->(item) { item["dir"] || item["location"] },
18
+ "registry" => ->(item) { item["path"] || item["location"] }
19
+ }.freeze
20
+
21
+ desc "Register a project and make it ready for team work " \
22
+ "(repository, private _overseer, lab config registry)"
23
+
24
+ example [
25
+ "atlas --path /Users/ace/lab/atlas --remote git@forge:atlas/atlas.git",
26
+ "atlas --path /Users/ace/lab/atlas --dry-run # Predict without mutating",
27
+ "atlas --path /Users/ace/lab/atlas --format json"
28
+ ]
29
+
30
+ argument :id, type: :string, required: true, desc: "Project id (topology id)"
31
+
32
+ option :path, type: :string, required: true, desc: "Absolute project path"
33
+ option :remote, type: :string, desc: "Origin remote URL (git clone source)"
34
+ option :label, type: :string, desc: "Display label recorded in the registry"
35
+ option :dry_run, type: :boolean, desc: "Predict changes without applying them"
36
+ option :format, type: :string, desc: "Output format (table, json; default: table)"
37
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
38
+
39
+ def initialize(project_add: nil)
40
+ super()
41
+ @project_add = project_add
42
+ end
43
+
44
+ def call(**options)
45
+ format = options[:format] || "table"
46
+ unless %w[table json].include?(format)
47
+ raise Ace::Support::Cli::Error,
48
+ "--format \"#{format}\" is not supported; table or json"
49
+ end
50
+
51
+ path = File.expand_path(options[:path])
52
+ organism = @project_add || Organisms::ProjectAdd.new
53
+ report = organism.call(
54
+ id: options[:id], path: path, remote: options[:remote],
55
+ label: options[:label], dry_run: options[:dry_run] || false
56
+ )
57
+ render(report, format: format, quiet: options[:quiet])
58
+
59
+ unless report["ok"] || report["dry_run"]
60
+ raise Ace::Support::Cli::Error, failure_summary(report)
61
+ end
62
+ rescue Ace::Lab::InvalidConfigurationError => e
63
+ raise Ace::Support::Cli::Error, e.message
64
+ end
65
+
66
+ private
67
+
68
+ def render(report, format:, quiet:)
69
+ return if quiet
70
+
71
+ puts format == "json" ? JSON.generate(report) : render_table(report)
72
+ end
73
+
74
+ def render_table(report)
75
+ lines = ["Project: #{report['project']} path: #{report['path']}" \
76
+ "#{" remote: #{report['remote']}" if report['remote']}" \
77
+ "#{" (dry run)" if report['dry_run']}"]
78
+ SECTION_NAMERS.each do |section, namer|
79
+ lines << "#{section.capitalize}:"
80
+ item = report[section]
81
+ detail = item["message"] ? " #{item['message']}" : ""
82
+ lines << " #{namer.call(item).to_s.ljust(34)} #{item['status']}#{detail}"
83
+ end
84
+ lines << "Result: #{report['ok'] ? 'ok' : 'FAILED'}"
85
+ lines.join("\n")
86
+ end
87
+
88
+ def failure_summary(report)
89
+ failures = SECTION_NAMERS.map do |section, namer|
90
+ item = report[section]
91
+ next unless item["status"] == "failed"
92
+
93
+ "#{section}/#{namer.call(item)}: #{item['message']}"
94
+ end.compact
95
+ "project add failed (#{failures.size} step(s)): #{failures.join('; ')}"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "ace/support/cli"
5
+ require "ace/core"
6
+ require_relative "support"
7
+
8
+ module Ace
9
+ module Lab
10
+ module CLI
11
+ module Commands
12
+ class Status < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+ include Runtime
15
+
16
+ desc <<~DESC.strip
17
+ Show lab configuration and local file state (read-only)
18
+
19
+ Distinguishes a valid empty set, missing configuration, and a
20
+ source error. Reads files only: no services are started and no
21
+ installer is required. The enriched worktree view stays
22
+ ace-git-worktree list.
23
+ DESC
24
+
25
+ example ["--format json # Full report as JSON"]
26
+
27
+ option :format, type: :string, default: "table", desc: "Output format: table or json"
28
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
29
+
30
+ def initialize(status_service: nil)
31
+ @status_service = status_service
32
+ end
33
+
34
+ def call(**options)
35
+ format = options[:format] || "table"
36
+ cli_error("Invalid format: #{format} (use table or json)") unless %w[table json].include?(format)
37
+
38
+ report = status_service.report
39
+ unless options[:quiet]
40
+ puts(format == "json" ? JSON.generate(report.to_h) : render_table(report))
41
+ end
42
+ # Full report first; source failures then fail the command
43
+ unless report.ok?
44
+ raise Ace::Support::Cli::Error, "lab status reported source errors: #{first_error(report)}"
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def status_service
51
+ @status_service ||= Organisms::StatusService.from_config
52
+ end
53
+
54
+ def first_error(report)
55
+ report.config[:error] ||
56
+ report.runtime_config[:error] ||
57
+ report.state.find { |location| location[:state] == :error }&.dig(:note) ||
58
+ "unreadable state location"
59
+ end
60
+
61
+ def render_table(report)
62
+ [config_line(report), runtime_line(report), "Local state (root: #{report.root}):"]
63
+ .concat(report.state.map { |location| location_line(location) })
64
+ .join("\n")
65
+ end
66
+
67
+ def config_line(report)
68
+ config = report.config
69
+ case config[:state]
70
+ when :configured
71
+ "Lab configuration: configured — #{config[:source_path]} " \
72
+ "(topology: #{config[:topology][:projects]} project(s), " \
73
+ "#{config[:topology][:agents]} agent(s), #{config[:topology][:services]} service(s))"
74
+ when :defaults_only
75
+ "Lab configuration: missing (defaults only; no .ace/lab/config.yml or ~/.ace/lab/config.yml)"
76
+ else
77
+ "Lab configuration: invalid — #{config[:error]}"
78
+ end
79
+ end
80
+
81
+ def runtime_line(report)
82
+ runtime = report.runtime_config
83
+ case runtime[:state]
84
+ when :valid
85
+ origin = runtime[:path] || runtime[:source]
86
+ "Runtime configuration: #{runtime[:runtime] || '(value unset)'} (#{origin})"
87
+ when :invalid
88
+ "Runtime configuration: invalid — #{runtime[:error]}"
89
+ else
90
+ "Runtime configuration: none (no config file, ACE_RUNTIME unset)"
91
+ end
92
+ end
93
+
94
+ def location_line(location)
95
+ state =
96
+ case location[:state]
97
+ when :present then format("present (%s)", location[:entries])
98
+ when :prepared then "prepared"
99
+ when :error then "ERROR"
100
+ else "not present"
101
+ end
102
+ note = location[:note] ? " (#{location[:note]})" : ""
103
+ format(" %-12s %-16s %s%s", location[:key], state, location[:path], note)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "ace/support/cli"
5
+ require "ace/core"
6
+ require_relative "../../organisms/verify"
7
+
8
+ module Ace
9
+ module Lab
10
+ module CLI
11
+ module Commands
12
+ class Verify < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ SECTION_LABELS = {
16
+ "accounts" => "account", "directories" => "directory", "keys" => "account",
17
+ "roles" => "id", "repositories" => "project", "grants" => "principal",
18
+ "registry" => "project", "overseer" => "project",
19
+ "adapters" => "adapter", "tools" => "tool", "gems" => "gem"
20
+ }.freeze
21
+
22
+ desc "Verify the installed lab's readiness under the target accounts (read-only): " \
23
+ "accounts, directories, key pairs, role instructions, repository origins and " \
24
+ "target-executor access, grant-based project visibility, private _overseer " \
25
+ "folders, configured communication/HITL adapters, required tools, declared gem set"
26
+
27
+ example [
28
+ "--format json # Machine-readable report for the overseer / next tool",
29
+ " # exit 0 only when every check passes or is unknown"
30
+ ]
31
+
32
+ option :format, type: :string, desc: "Output format (table, json; default: table)"
33
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
34
+
35
+ def initialize(verifier: nil)
36
+ super()
37
+ @verifier = verifier
38
+ end
39
+
40
+ def call(**options)
41
+ format = options[:format] || "table"
42
+ unless %w[table json].include?(format)
43
+ raise Ace::Support::Cli::Error,
44
+ "--format \"#{format}\" is not supported; table or json"
45
+ end
46
+
47
+ report = (@verifier || Organisms::Verify.new).call
48
+ render(report, format: format, quiet: options[:quiet])
49
+
50
+ # A not-ready lab never exits 0: the report renders first
51
+ # (failure beside healthy data — both visible), then the
52
+ # failure list names every failed check concretely.
53
+ unless report["ready"]
54
+ raise Ace::Support::Cli::Error, failure_summary(report)
55
+ end
56
+ rescue Ace::Lab::InvalidConfigurationError => e
57
+ raise Ace::Support::Cli::Error, e.message
58
+ end
59
+
60
+ private
61
+
62
+ def render(report, format:, quiet:)
63
+ return if quiet
64
+
65
+ puts format == "json" ? JSON.generate(report) : render_table(report)
66
+ end
67
+
68
+ def render_table(report)
69
+ counts = report["counts"]
70
+ lines = ["Machine: #{report['machine']} manifest: #{report['manifest']}"]
71
+ report["checks"].each do |section, checks|
72
+ lines << "#{section.capitalize}:"
73
+ if checks.empty?
74
+ lines << " (none declared)"
75
+ next
76
+ end
77
+ checks.each do |check|
78
+ label = check[SECTION_LABELS.fetch(section, "reason")] || check["path"] || section
79
+ detail = detail_of(check)
80
+ lines << " #{label.to_s.ljust(34)} #{check['status'].upcase}#{detail}"
81
+ lines << " #{check['reason']}" if check["reason"]
82
+ end
83
+ end
84
+ lines << "Checks: #{counts['pass']} pass, #{counts['fail']} failed, " \
85
+ "#{counts['unknown']} unknown"
86
+ lines << "Result: #{report['ready'] ? 'ready' : 'NOT READY'}"
87
+ lines.join("\n")
88
+ end
89
+
90
+ def detail_of(check)
91
+ parts = []
92
+ parts << "uid #{check['uid']}" if check.key?("uid")
93
+ parts << "shell #{check['shell']}" if check["shell"]
94
+ parts << "fingerprint #{check['fingerprint']}" if check["fingerprint"]
95
+ parts << "origin #{check['origin']}" if check["origin"]
96
+ parts << "projects #{check['projects'].join(', ')}" if check["projects"].is_a?(Array)
97
+ parts.empty? ? "" : " (#{parts.join(', ')})"
98
+ end
99
+
100
+ def failure_summary(report)
101
+ failures = report["checks"].flat_map do |section, checks|
102
+ checks.select { |check| check["status"] == "fail" }
103
+ .map { |check| "#{section}/#{check_name(check, section)}: #{check['reason']}" }
104
+ end
105
+ "lab is not ready (#{failures.size} failed check(s)): #{failures.join('; ')}"
106
+ end
107
+
108
+ def check_name(check, section)
109
+ check[SECTION_LABELS.fetch(section, "reason")] || check["path"] || section
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
data/lib/ace/lab/cli.rb CHANGED
@@ -8,10 +8,11 @@ require_relative "cli/commands/agents"
8
8
  require_relative "cli/commands/services"
9
9
  require_relative "cli/commands/resolve"
10
10
  require_relative "cli/commands/route"
11
- require_relative "cli/commands/service"
12
- require_relative "cli/commands/assignment"
13
- require_relative "cli/commands/command_execute"
14
- require_relative "cli/commands/run"
11
+ require_relative "cli/commands/status"
12
+ require_relative "cli/commands/prepare"
13
+ require_relative "cli/commands/project_add"
14
+ require_relative "cli/commands/install"
15
+ require_relative "cli/commands/verify"
15
16
 
16
17
  module Ace
17
18
  module Lab
@@ -23,26 +24,28 @@ module Ace
23
24
 
24
25
  # Application commands with descriptions (for help output)
25
26
  REGISTERED_COMMANDS = [
26
- ["run", "Run one explicitly authorized project command"],
27
- ["run-status", "Inspect an original command execution without running it"],
28
27
  ["projects", "List lab projects visible to the verified caller"],
29
28
  ["agents", "List lab agents in a project by stable ID"],
30
29
  ["services", "List lab services in a project by stable ID"],
31
30
  ["resolve", "Resolve one lab entry by exact stable ID"],
32
31
  ["route", "Select a configured capable service in a project"],
33
- ["service request", "Request one authorized service operation"],
34
- ["service status", "Read a service request outcome"],
35
- ["service listen", "Serve the ordinary fixed merge receiver on its configured socket"],
36
- ["assignment prepare", "Retain reviewed task input for ordinary Assign registration"],
37
- ["assignment drive", "Drive one original execution through the installed ordinary owner"],
38
- ["assignment request", "Send one retained operation to the installed ordinary Assign owner"]
32
+ ["status", "Show lab configuration and local file state (read-only)"],
33
+ ["prepare", "Prepare accounts, directories, permissions, keys and roles from the trusted install manifest"],
34
+ ["project add", "Register a project and make it ready for team work (repository, _overseer, lab config)"],
35
+ ["install", "Perform the full new-lab installation from the trusted install manifest"],
36
+ ["verify", "Verify the installed lab's readiness under the target accounts (read-only)"]
39
37
  ].freeze
40
38
 
41
39
  HELP_EXAMPLES = [
42
40
  "ace-lab projects --format json",
43
41
  "ace-lab agents --project atlas --format json",
44
42
  "ace-lab resolve --id atlas-planner --format json",
45
- "ace-lab route --project atlas --capability search --format json"
43
+ "ace-lab route --project atlas --capability search --format json",
44
+ "ace-lab status --format json",
45
+ "ace-lab prepare --dry-run",
46
+ "ace-lab project add atlas --path /Users/ace/lab/atlas --remote git@forge:atlas/atlas.git",
47
+ "ace-lab install --dry-run",
48
+ "ace-lab verify --format json"
46
49
  ].freeze
47
50
 
48
51
  # Start the CLI
@@ -50,7 +53,6 @@ module Ace
50
53
  # @param args [Array<String>] Command-line arguments
51
54
  # @return [Integer] Exit code (0 for success, non-zero for failure)
52
55
  def self.start(args)
53
- return CLI::Commands::Run.invoke(args.drop(1)) if args.first == "run" && !(args.take(args.index("--") || args.size).include?("--help") || args.take(args.index("--") || args.size).include?("-h"))
54
56
  Ace::Support::Cli::Runner.new(self).call(args: args)
55
57
  end
56
58
 
@@ -60,15 +62,11 @@ module Ace
60
62
  register "services", CLI::Commands::Services.new
61
63
  register "resolve", CLI::Commands::Resolve.new
62
64
  register "route", CLI::Commands::Route.new
63
- register "service request", CLI::Commands::Service::Request.new
64
- register "service status", CLI::Commands::Service::Status.new
65
- register "service listen", CLI::Commands::Service::Listen.new
66
- register "run", CLI::Commands::Run.new
67
- register "run-status", CLI::Commands::RunStatus.new
68
- register "command execute", CLI::Commands::CommandExecute.new
69
- register "assignment prepare", CLI::Commands::Assignment::Prepare.new
70
- register "assignment drive", CLI::Commands::Assignment::Drive.new
71
- register "assignment request", CLI::Commands::Assignment::Request.new
65
+ register "status", CLI::Commands::Status.new
66
+ register "prepare", CLI::Commands::Prepare.new
67
+ register "project add", CLI::Commands::ProjectAdd.new
68
+ register "install", CLI::Commands::Install.new
69
+ register "verify", CLI::Commands::Verify.new
72
70
 
73
71
  # Register version command
74
72
  version_cmd = Ace::Support::Cli::VersionCommand.build(
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Lab
5
+ # The trusted install manifest is a deployment document at a FIXED path
6
+ # — never in the caller-writable configuration cascade and never at a
7
+ # caller-selected location. It must be root-owned and not
8
+ # group/world-writable along its whole path, exactly like
9
+ # AUTHORIZATION_PATH; InstallManifestLoader verifies through
10
+ # GrantResolver.read_verified and fails closed. A caller-writable
11
+ # manifest could provision accounts, directories and keys, i.e.
12
+ # manufacture privileges.
13
+ INSTALL_MANIFEST_PATH = "/etc/lab/ace-lab/install.yml"
14
+
15
+ # Path of the trusted install manifest. A method (not a bare constant
16
+ # reference) so tests stub the seam without any caller-controllable
17
+ # production override; the CLI deliberately exposes no --manifest flag.
18
+ # @return [String]
19
+ def self.install_manifest_path
20
+ INSTALL_MANIFEST_PATH
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Lab
5
+ module Models
6
+ # Immutable value object carrying the assembled status report.
7
+ # ok? is false exactly when a configuration source is invalid or a
8
+ # state location failed to read — an empty-but-valid lab is ok.
9
+ class StatusReport
10
+ attr_reader :config, :runtime_config, :state, :root
11
+
12
+ def initialize(config:, runtime_config:, state:, root:)
13
+ @config = config.freeze
14
+ @runtime_config = runtime_config.freeze
15
+ @state = state.map(&:freeze).freeze
16
+ @root = root
17
+ freeze
18
+ end
19
+
20
+ def ok?
21
+ @config[:state] != :invalid &&
22
+ @runtime_config[:state] != :invalid &&
23
+ @state.none? { |location| location[:state] == :error }
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ config: @config,
29
+ runtime_config: @runtime_config,
30
+ state: @state,
31
+ root: @root,
32
+ ok: ok?
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end