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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +363 -104
  3. data/README.md +66 -100
  4. data/docs/usage.md +529 -592
  5. data/lib/ace/lab/atoms/install_schema.rb +373 -0
  6. data/lib/ace/lab/cli/commands/install.rb +161 -0
  7. data/lib/ace/lab/cli/commands/prepare.rb +118 -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/grant_provisioner.rb +250 -0
  18. data/lib/ace/lab/molecules/grant_resolver.rb +34 -23
  19. data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
  20. data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
  21. data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
  22. data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
  23. data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
  24. data/lib/ace/lab/molecules/project_registry.rb +251 -0
  25. data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
  26. data/lib/ace/lab/molecules/role_verifier.rb +223 -0
  27. data/lib/ace/lab/organisms/install.rb +213 -0
  28. data/lib/ace/lab/organisms/prepare.rb +156 -0
  29. data/lib/ace/lab/organisms/project_add.rb +170 -0
  30. data/lib/ace/lab/organisms/status_service.rb +70 -0
  31. data/lib/ace/lab/organisms/verify.rb +768 -0
  32. data/lib/ace/lab/version.rb +1 -1
  33. data/lib/ace/lab.rb +34 -6
  34. metadata +34 -108
  35. data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
  36. data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
  37. data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
  38. data/lib/ace/lab/atoms/service_input.rb +0 -92
  39. data/lib/ace/lab/cli/commands/assignment.rb +0 -92
  40. data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
  41. data/lib/ace/lab/cli/commands/run.rb +0 -65
  42. data/lib/ace/lab/cli/commands/service.rb +0 -240
  43. data/lib/ace/lab/models/service_receipt.rb +0 -18
  44. data/lib/ace/lab/molecules/command_owner_admission.rb +0 -183
  45. data/lib/ace/lab/molecules/command_policy.rb +0 -158
  46. data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
  47. data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
  48. data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
  49. data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
  50. data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
  51. data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -152
  52. data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
  53. data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
  54. data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
  55. data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
  56. data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
  57. data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
  58. data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
  59. data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
  60. data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
  61. data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
  62. data/lib/ace/lab/molecules/service_executor.rb +0 -99
  63. data/lib/ace/lab/molecules/service_policy.rb +0 -289
  64. data/lib/ace/lab/organisms/authority_composition.rb +0 -87
  65. data/lib/ace/lab/organisms/command_executor.rb +0 -220
  66. data/lib/ace/lab/organisms/command_run_client.rb +0 -131
  67. data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
  68. data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
  69. data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
  70. data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
  71. data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
  72. data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
  73. data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
  74. data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
  75. data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
  76. data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
  77. data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
  78. data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
  79. data/lib/ace/lab/organisms/service_request_service.rb +0 -249
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../organisms/ordinary_assignment_client"
4
- require "ace/assign/organisms/ordinary_driver"
5
-
6
- module Ace
7
- module Lab
8
- module CLI
9
- module Commands
10
- module Assignment
11
- class Drive < Ace::Support::Cli::Command
12
- desc "Drive or inspect one original ordinary execution through its installed owner"
13
- option :project, required: true, desc: "Installed project ID; never inferred from local Assign"
14
- option :context, required: true, desc: "Original retained ordinary context.json"
15
- option :operation, required: true, desc: "status, supervisor, adopt-supervisor, bootstrap, next, start, step-start, step-finish, fail, result, adopt, replay"
16
- option :previous_binding_sha256, desc: "Exact canonical old supervisor binding digest for adoption"
17
- option :output, desc: "New private directory for accepted bootstrap context"
18
- option :pane, desc: "Actual native caller pane (defaults to HERDR_PANE)"
19
- option :generation, type: :integer, desc: "Exact canonical generation observed before mutation"
20
- option :step, desc: "Exact queue step within the retained scope"
21
- option :report, desc: "Executed step's bounded UTF-8 report file"
22
- option :message, desc: "Failure reason"
23
- option :receipt, desc: "Attributable execution receipt JSON"
24
- option :artifact, type: :array, desc: "Artifact files in receipt order"
25
- option :request, desc: "Original mutation request to replay after reconciliation"
26
-
27
- def call(project:, context:, operation:, **options)
28
- transport = Organisms::OrdinaryAssignmentClient.new(project: project)
29
- driver = Ace::Assign::Organisms::OrdinaryDriver.new(path: context, project: project, transport: transport)
30
- pane = options[:pane] || ENV["HERDR_PANE"]
31
- result = case operation
32
- when "status" then driver.status
33
- when "supervisor" then driver.supervisor
34
- when "adopt-supervisor"
35
- driver.adopt_supervisor(pane: pane, generation: options.fetch(:generation),
36
- previous_binding_sha256: options.fetch(:previous_binding_sha256))
37
- when "bootstrap" then driver.bootstrap(output: options.fetch(:output))
38
- when "next" then driver.queue
39
- when "start" then driver.start(pane: pane)
40
- when "step-start", "step-finish", "fail"
41
- driver.progress(operation: {"step-start" => "queue_start", "step-finish" => "queue_finish", "fail" => "queue_fail"}.fetch(operation),
42
- pane: pane, generation: options.fetch(:generation), step: options[:step],
43
- report: operation == "step-finish" ? options.fetch(:report) : nil,
44
- message: operation == "fail" ? options.fetch(:message) : nil)
45
- when "result"
46
- driver.submit(pane: pane, generation: options.fetch(:generation), receipt: options.fetch(:receipt), artifacts: options[:artifact] || [])
47
- when "adopt" then driver.adopt(pane: pane)
48
- when "replay" then driver.replay(input: options.fetch(:request))
49
- else raise ArgumentError, "unknown ordinary driver operation"
50
- end
51
- puts JSON.generate(result)
52
- rescue ArgumentError, KeyError, SystemCallError, Ace::Assign::Error, Ace::Runtime::Error, Ace::Lab::Error => error
53
- raise Ace::Support::Cli::Error, "ordinary execution unavailable: #{error.message}; inspect the original context with --operation status"
54
- end
55
- end
56
-
57
- class Prepare < Ace::Support::Cli::Command
58
- desc "Retain reviewed task input for ordinary Assign registration"
59
- option :project, required: true, desc: "Installed project ID"
60
- option :task, required: true, desc: "Reviewed leaf task ID"
61
- option :pane, required: true, desc: "Actual supervisor pane for registration"
62
- option :output, required: true, desc: "New private directory for retained input"
63
- option :dependency_reports, desc: "JSON array of explicit dependency report selections"
64
-
65
- def call(project:, task:, pane:, output:, **options)
66
- reports = options[:dependency_reports] ? JSON.parse(Ace::Assign::Authority::RetainedInput.read_file(
67
- options.fetch(:dependency_reports), 65_536), allow_duplicate_key: false, create_additions: false, max_nesting: 16) : []
68
- puts JSON.generate(Organisms::OrdinaryAssignmentClient.prepare(project: project, task: task,
69
- pane: pane, output: output, dependency_reports: reports))
70
- rescue JSON::ParserError, ArgumentError, KeyError, SystemCallError, Ace::Assign::Error, Ace::Runtime::Error => error
71
- raise Ace::Support::Cli::Error, "ordinary preparation incomplete: #{error.message}; retain existing files"
72
- end
73
- end
74
-
75
- class Request < Ace::Support::Cli::Command
76
- desc "Send one retained operation to the installed ordinary Assign owner"
77
- option :project, required: true, desc: "Installed project ID"
78
- option :input, required: true, desc: "Retained request JSON with original mutation and part digests"
79
- option :output, desc: "New private directory for a canonical snapshot download"
80
-
81
- def call(project:, input:, **options)
82
- service = Organisms::OrdinaryAssignmentClient.new(project: project)
83
- puts JSON.generate(service.request(input: input, output: options[:output]))
84
- rescue ArgumentError, KeyError, SystemCallError, Ace::Assign::Error, Ace::Runtime::Error, Ace::Lab::Error => error
85
- raise Ace::Support::Cli::Error, "ordinary assignment unavailable: #{error.message}; retain the original request; inspect status before new work"
86
- end
87
- end
88
- end
89
- end
90
- end
91
- end
92
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
- require_relative "../../organisms/command_executor"
3
- module Ace
4
- module Lab
5
- module CLI
6
- module Commands
7
- class CommandExecute < Ace::Support::Cli::Command
8
- desc "Execute one admitted command for the installed root launcher"
9
- option :project, required: true
10
- option :fd, type: :integer, required: true
11
- def call(project:, fd:, **)
12
- raise SecurityError unless Process.uid.zero? && Process.euid.zero? && fd >= 3
13
- bytes = $stdin.read(16_385)
14
- raise ArgumentError if bytes.bytesize > 16_384
15
- request = JSON.parse(bytes, allow_duplicate_key: false, create_additions: false, max_nesting: 16)
16
- socket = UNIXSocket.for_fd(fd)
17
- Organisms::CommandExecutor.new(project: project).run(socket: socket, request: request)
18
- 0 # Child status belongs to the framed result, not helper transport.
19
- rescue StandardError, SecurityError
20
- raise Ace::Support::Cli::Error, "command uncertain; inspect original execution reference"
21
- ensure
22
- socket&.close unless socket&.closed?
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
- require "optparse"
3
- require_relative "../../organisms/command_run_client"
4
- module Ace
5
- module Lab
6
- module CLI
7
- module Commands
8
- class Run < Ace::Support::Cli::Command
9
- desc "Run one explicitly authorized project command"
10
- option :project, required: true, desc: "Installed project ID"
11
- option :as, desc: "Configured permitted executor"
12
- option :service, repeat: true, desc: "Replace default services with this complete set"
13
- argument :argv, type: :array, desc: "Literal command argv after --"
14
-
15
- def self.parse(args)
16
- separator = args.index("--")
17
- raise ArgumentError, "run requires -- followed by literal command argv" unless separator && separator < args.size - 1
18
- switches, argv = args.first(separator), args.drop(separator + 1)
19
- options = {executor: nil, services: nil}
20
- seen = {}
21
- parser = OptionParser.new
22
- %w[project as].each do |name|
23
- parser.on("--#{name} VALUE") do |value|
24
- raise ArgumentError, "duplicate --#{name}" if seen[name]
25
- seen[name] = true
26
- options[name == "as" ? :executor : :project] = value
27
- end
28
- end
29
- parser.on("--service VALUE") { |value| (options[:services] ||= []) << value }
30
- parser.parse!(switches)
31
- raise ArgumentError, "run requires one --project and no extra options" unless seen["project"] && switches.empty?
32
- options.merge(argv: argv)
33
- rescue OptionParser::ParseError, ArgumentError
34
- raise Ace::Support::Cli::Error, "usage_error: run requires unique project/executor options and literal argv after --"
35
- end
36
-
37
- def self.invoke(args)
38
- options = parse(args)
39
- project = options.delete(:project)
40
- client = Organisms::CommandRunClient.new(project: project)
41
- begin
42
- client.run(**options, pane: ENV["HERDR_PANE_ID"])
43
- rescue StandardError, SecurityError
44
- # Once admission may have crossed the owner boundary, even a
45
- # malformed stream is uncertainty, never a fresh usage refusal.
46
- raise Ace::Support::Cli::Error, "command_uncertain: inspect the original execution reference before new work"
47
- end
48
- rescue Ace::Lab::InvalidConfigurationError
49
- raise Ace::Support::Cli::Error, "invalid_command_context: installed project selection is unavailable"
50
- end
51
- end
52
- class RunStatus < Ace::Support::Cli::Command
53
- desc "Inspect an original command execution without running it"
54
- option :project, required: true
55
- option :reference, required: true
56
- def call(project:, reference:, **)
57
- puts JSON.generate(Organisms::CommandRunClient.new(project: project).status(reference))
58
- rescue StandardError, SecurityError
59
- raise Ace::Support::Cli::Error, "command_status_unavailable: preserve original execution reference"
60
- end
61
- end
62
- end
63
- end
64
- end
65
- end
@@ -1,240 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "ace/support/cli"
5
- require "ace/assign"
6
- require_relative "support"
7
- require_relative "../../organisms/protected_service_request"
8
- require_relative "../../organisms/ordinary_merge_request"
9
- require_relative "../../organisms/ordinary_merge_receiver"
10
- require_relative "../../organisms/ordinary_authority_composition"
11
- require_relative "../../molecules/ordinary_listener"
12
- require_relative "../../molecules/ordinary_receiver_config"
13
-
14
- module Ace
15
- module Lab
16
- module CLI
17
- module Commands
18
- module Service
19
- module Output
20
- def service
21
- @service ||= Organisms::ServiceRequestService.new
22
- end
23
-
24
- def protected_service
25
- @protected_service ||= Organisms::ProtectedServiceRequest.new
26
- end
27
-
28
- def protected_refusal(error)
29
- code = (error.is_a?(ArgumentError) || error.is_a?(SecurityError)) ? "protected_service_refused" : "protected_service_unavailable"
30
- message = error.is_a?(JSON::ParserError) ? "protected installed selection unavailable" : error.message.to_s.scrub[0, 512]
31
- emit({"status" => "error", "error" => {"code" => code, "message" => message}})
32
- end
33
-
34
- def emit(envelope)
35
- puts JSON.generate(envelope)
36
- return if envelope["status"] == "ok"
37
- error = envelope.fetch("error")
38
- code = error.fetch("code")
39
- message = error.fetch("message")
40
- raise Ace::Support::Cli::Error, "#{code}: #{message}"
41
- end
42
- end
43
-
44
- class Request < Ace::Support::Cli::Command
45
- include Ace::Support::Cli::Base
46
- include Runtime
47
- include Output
48
-
49
- desc "Request one authorized, configured service operation"
50
- option :project, type: :string, required: true, desc: "Project stable ID"
51
- option :assignment, type: :string, required: true, desc: "Assignment ID"
52
- option :attempt, type: :string, required: true, desc: "Managed attempt ID"
53
- option :operation, type: :string, required: true, desc: "Named operation"
54
- option :input, type: :string, required: true, desc: "Structured JSON input file"
55
- option :authorization, type: :string, required: true, desc: "Exact authorization reference"
56
- option :request_id, type: :string, required: true, desc: "Idempotency key"
57
- option :dry_run, type: :boolean, desc: "Validate without claiming or executing"
58
- option :mode, type: :string, desc: "ordinary-merge selects the ordinary fixed merge producer"
59
- option :queue_entry, type: :string, desc: "Original queue entry (ordinary merge)"
60
-
61
- option :mapping, type: :string, desc: "Original protected mapping ID"
62
- option :scope, type: :string, desc: "Original hierarchical prepared scope"
63
- option :service, type: :string, desc: "Installed receiver ID"
64
- option :candidate_head, type: :string, desc: "Exact approved candidate head"
65
- option :candidate_generation, type: :integer, desc: "Original candidate generation"
66
- option :expected_generation, type: :integer, desc: "Original authority generation"
67
-
68
- def call(project:, assignment:, attempt:, operation:, input:, authorization:, request_id:, **options)
69
- reject_identity_flags!(options)
70
- mode = options[:mode]
71
- if mode && mode != Organisms::OrdinaryMergeRequest::MODE
72
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope("ordinary_merge_invalid_request",
73
- "unknown service mode", request_id: request_id, input_digest: nil))
74
- end
75
- if mode
76
- begin
77
- return emit(ordinary_request(project: project, assignment: assignment, attempt: attempt,
78
- queue_entry: options[:queue_entry], operation: operation, input: input,
79
- authorization: authorization, request_id: request_id, **options))
80
- rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
81
- # Infra failures (unreadable canonical evidence, malformed
82
- # installed configuration, unavailable topology) are the
83
- # caller-side blocker, never a raw crash and never a durable
84
- # reference (review r1 finding B).
85
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
86
- "ordinary_merge_unavailable", e.message, request_id: request_id, input_digest: nil
87
- ))
88
- end
89
- end
90
- selected = protected_service.selected?(options)
91
- if selected
92
- return emit(protected_service.request(project: project, assignment: assignment, attempt: attempt,
93
- operation: operation, input_path: input, authorization: authorization, request_id: request_id, **options))
94
- end
95
- emit(service.request(project: project, assignment: assignment, attempt: attempt,
96
- operation: operation, input_path: input, authorization: authorization,
97
- request_id: request_id, dry_run: options[:dry_run]))
98
- rescue ArgumentError, Ace::Assign::Error, Ace::Git::Error, Ace::Runtime::RuntimeUnavailableError, JSON::ParserError, SecurityError, KeyError, SystemCallError => error
99
- raise if selected == false || mode
100
- protected_refusal(error)
101
- end
102
-
103
- private
104
-
105
- def ordinary_producer
106
- @ordinary_producer ||= Organisms::OrdinaryMergeRequest.new
107
- end
108
-
109
- # The ordinary refusal envelope is emitted exactly as received;
110
- # it is classified for the non-zero exit like every refusal.
111
- def ordinary_request(**arguments)
112
- ordinary_producer.request(**arguments)
113
- end
114
- end
115
-
116
- class Status < Ace::Support::Cli::Command
117
- include Ace::Support::Cli::Base
118
- include Runtime
119
- include Output
120
-
121
- desc "Read the authoritative outcome of a service request"
122
- option :request, type: :string, required: true, desc: "Request ID"
123
- option :format, type: :string, desc: "Output format (json only)"
124
- option :mode, type: :string, desc: "ordinary-merge selects the read-only ordinary status"
125
- option :reference, type: :string, desc: "Opaque ordinary merge reference"
126
- option :queue_entry, type: :string, desc: "Original queue entry (ordinary merge)"
127
-
128
- option :project, type: :string, desc: "Original project ID"
129
- option :assignment, type: :string, desc: "Original assignment ID"
130
- option :attempt, type: :string, desc: "Original attempt ID"
131
- option :mapping, type: :string, desc: "Original protected mapping ID"
132
- option :scope, type: :string, desc: "Original hierarchical prepared scope"
133
- option :candidate_head, type: :string, desc: "Exact approved candidate head"
134
- option :candidate_generation, type: :integer, desc: "Original candidate generation"
135
- option :input_digest, type: :string, desc: "Original normalized input SHA-256"
136
- option :target, type: :string, desc: "Original target resource ID"
137
- option :artifact_digest, type: :string, desc: "Original artifact SHA-256 (omit for null)"
138
-
139
- def call(request:, **options)
140
- reject_identity_flags!(options)
141
- mode = options[:mode]
142
- if mode && mode != Organisms::OrdinaryMergeRequest::MODE
143
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope("ordinary_merge_invalid_request",
144
- "unknown service mode", request_id: request, input_digest: nil,
145
- schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID))
146
- end
147
- if mode
148
- ensure_json_format!(options)
149
- begin
150
- return emit(ordinary_producer.status(project: options[:project], assignment: options[:assignment],
151
- attempt: options[:attempt], queue_entry: options[:queue_entry], reference: options[:reference],
152
- request: request, input_digest: options[:input_digest], **options))
153
- rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
154
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
155
- "ordinary_merge_unavailable", e.message, request_id: request, input_digest: nil,
156
- schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID
157
- ))
158
- end
159
- end
160
- selected = protected_service.selected?(options)
161
- if selected
162
- return emit(protected_service.status(request_id: request, **options))
163
- end
164
- emit(service.status(request_id: request))
165
- rescue ArgumentError, Ace::Assign::Error, Ace::Git::Error, Ace::Runtime::RuntimeUnavailableError, JSON::ParserError, SecurityError, KeyError, SystemCallError => error
166
- raise if selected == false || mode
167
- protected_refusal(error)
168
- end
169
-
170
- private
171
-
172
- def ordinary_producer
173
- @ordinary_producer ||= Organisms::OrdinaryMergeRequest.new
174
- end
175
- end
176
-
177
- # The ordinary receiver serve entrypoint (spec 8x3.t.xz9.4
178
- # execution clauses; 8x3.t.xz9.5 integration handoff): binds the
179
- # configured unix socket, mutually authenticates every connection
180
- # through OS peer credentials, admits through the canonical Assign
181
- # coordinator, executes the authorized fixed operation — one REAL
182
- # Forgejo merge under the scoped host-installed service identity —
183
- # and settles the exact terminal receipt proven by real forge
184
- # readback. Configuration is one explicit deployment-owned file
185
- # plus the host-installed authorization and forge-merge documents;
186
- # the host owns their placement and credentials. Blocks until
187
- # stopped.
188
- class Listen < Ace::Support::Cli::Command
189
- include Ace::Support::Cli::Base
190
- include Output
191
-
192
- desc "Serve ordinary Assign, HITL and fixed merge under the installed journal owner"
193
- option :mode, type: :string, required: true,
194
- desc: "ordinary-merge selects the ordinary fixed merge receiver"
195
- option :project, type: :string, required: true, desc: "Project stable ID"
196
- option :config, type: :string, required: true,
197
- desc: "Deployment-owned receiver configuration file (YAML)"
198
-
199
- def call(mode:, project:, config:, **options)
200
- unless options.empty?
201
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
202
- "ordinary_merge_invalid_request",
203
- "unknown listen selector: #{options.keys.sort.join(", ")}",
204
- request_id: "unknown", input_digest: nil
205
- ))
206
- end
207
- unless mode == Organisms::OrdinaryMergeRequest::MODE
208
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
209
- "ordinary_merge_invalid_request", "unknown service mode",
210
- request_id: "unknown", input_digest: nil
211
- ))
212
- end
213
- receiver_config = Molecules::OrdinaryReceiverConfig.load(config)
214
- unless receiver_config.project == project
215
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
216
- "ordinary_merge_selection_mismatch",
217
- "configured project does not match the requested project",
218
- request_id: "unknown", input_digest: nil
219
- ))
220
- end
221
- unless receiver_config.receiver_uid == Process.uid
222
- raise Ace::Lab::InvalidConfigurationError, "receiver must run as its configured UID"
223
- end
224
- server = Organisms::OrdinaryAuthorityComposition.new(receiver_config: receiver_config).build
225
- signals = %w[INT TERM].to_h { |signal| [signal, Signal.trap(signal) { server.request_stop }] }
226
- server.serve
227
- rescue Ace::Lab::InvalidConfigurationError, Ace::Assign::Error, Ace::Runtime::Error, Ace::Hitl::Lifecycle::Error => e
228
- emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
229
- "ordinary_merge_unavailable", e.message,
230
- request_id: "unknown", input_digest: nil
231
- ))
232
- ensure
233
- signals&.each { |signal, handler| Signal.trap(signal, handler) }
234
- end
235
- end
236
- end
237
- end
238
- end
239
- end
240
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Lab
5
- module Models
6
- # Non-secret result submitted to the assignment evidence coordinator.
7
- # The coordinator independently verifies every binding before acceptance.
8
- module ServiceReceipt
9
- BINDING_FIELDS = %w[request_id assignment_id attempt_id project_id operation
10
- input_digest target candidate_head executor_uid transport].freeze
11
-
12
- def self.build(binding, executor_result)
13
- BINDING_FIELDS.to_h { |field| [field, binding.fetch(field)] }.merge(executor_result)
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,183 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "find"
4
- require "ace/assign/authority/command_execution"
5
- require "ace/herdr/molecules/bounded_observation"
6
- require "ace/runtime/molecules/process_identity"
7
- require_relative "command_policy"
8
-
9
- module Ace
10
- module Lab
11
- module Molecules
12
- # Adds the project-command principals to the same installed listener.
13
- # Root launch facts do not impersonate a participant/native agent.
14
- class CommandOwnerAdmission
15
- def initialize(base:, project:, policy_loader: nil)
16
- @base, @project = base, project
17
- @policy_loader = policy_loader || -> { CommandPolicy.load(project: @project) }
18
- @peer = Ace::Assign::Authority::OrdinaryPeer.new
19
- end
20
- def service = @base.service
21
- def verify_listener! = @base.verify_listener!
22
- def peer(socket, deadline:) = @base.peer(socket, deadline: deadline)
23
- def authorize!(request:, peer:, deadline:)
24
- return @base.authorize!(request: request, peer: peer, deadline: deadline) unless Ace::Assign::Authority::CommandExecution::OPERATIONS.include?(request["operation"])
25
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity unless request["project_id"] == @project
26
- if %w[command_claim command_bind command_complete].include?(request["operation"])
27
- command_launcher!(peer: peer, deadline: deadline)
28
- elsif request["operation"] == "command_status"
29
- @peer.live!(peer, deadline: deadline) # Handler checks original requester before exposing data.
30
- else
31
- command_reader!(peer: peer, deadline: deadline)
32
- end
33
- :worker
34
- end
35
-
36
- def command_reader!(peer:, deadline:, definition: nil)
37
- @peer.live!(peer, deadline: deadline)
38
- return true if peer["uid"] == 0 && command_launcher!(peer: peer, deadline: deadline)
39
- if definition
40
- # Inspection never grants a second effect. Original requester UID
41
- # retains reconciliation after policy revocation or process restart.
42
- unless definition.dig("requester", "uid") == peer.fetch("uid")
43
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity, "command reference belongs to another requester"
44
- end
45
- return true
46
- end
47
- # Authorize visibility through the same closed selection machinery;
48
- # absence of any configured grant is a refusal, not native inference.
49
- document = policy
50
- document.reader!(peer.fetch("uid"))
51
- end
52
-
53
- def command_request!(selection:, peer:, deadline:)
54
- @peer.live!(peer, deadline: deadline)
55
- selected = policy.select(selection: selection, peer: peer)
56
- if selected.fetch("requester_kind") == "agent"
57
- selected["native_binding"] = @base.identity!(peer: peer, pane: selection.fetch("pane"), deadline: deadline).runtime_binding
58
- end
59
- verify_context!(selected, deadline)
60
- selected
61
- end
62
-
63
- # Historical execution can outlive its requester. Current root policy
64
- # and immutable source/executable must still grant this exact TEST
65
- # command; requester liveness does not invent a new execution.
66
- def command_test_evidence!(definition:, deadline:)
67
- unless definition["purpose"] == "test" && definition.dig("source", "head") && definition.dig("source", "tree") &&
68
- policy.select(selection: definition.fetch("selection"), peer: definition.fetch("requester")) == definition.except("native_binding")
69
- raise Ace::Assign::AttemptErrors::ReceiptRejected, "executed test command policy was revoked or changed"
70
- end
71
- verify_context!(definition, deadline)
72
- rescue SecurityError, Ace::Lab::InvalidConfigurationError
73
- raise Ace::Assign::AttemptErrors::ReceiptRejected, "executed test command policy or source is unavailable"
74
- end
75
-
76
- def command_launcher!(peer:, deadline:)
77
- @peer.live!(peer, deadline: deadline)
78
- expected = JSON.parse(GrantResolver.read_verified(policy.launcher.fetch("identity_path")) || "null")
79
- unless expected.is_a?(Hash) && expected.keys.sort == %w[identity schema] &&
80
- expected["schema"] == "labd-command-launcher/v1" && expected["identity"].is_a?(Hash) &&
81
- expected["identity"]["uid"] == 0 && peer["uid"] == 0
82
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity, "installed command launcher differs"
83
- end
84
- target = expected.fetch("identity")
85
- @peer.live!(target, deadline: deadline)
86
- cursor = peer
87
- 64.times do
88
- return target if cursor.slice("pid", "uid", "host", "started_at") == target.slice("pid", "uid", "host", "started_at")
89
- break unless RUBY_PLATFORM.include?("linux") && cursor["uid"] == 0
90
- stat = File.read("/proc/#{cursor.fetch('pid')}/stat")
91
- parent = Integer(stat[(stat.rindex(")") + 2)..].split.fetch(1), 10)
92
- break if parent <= 1
93
- @peer.live!(cursor, deadline: deadline)
94
- identity = Ace::Runtime::Molecules::ProcessIdentity.new(executor:
95
- Ace::Herdr::Molecules::BoundedObservation.new(deadline: deadline)).capture(parent)
96
- break unless identity
97
- cursor = identity
98
- end
99
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity, "command helper is outside installed launcher ancestry"
100
- rescue JSON::ParserError, SystemCallError, IndexError, ArgumentError
101
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity, "installed command launcher is unavailable"
102
- end
103
-
104
- def command_claim!(definition:, caller:, launcher:, deadline:)
105
- command_launcher!(peer: launcher, deadline: deadline)
106
- unless caller == definition.fetch("requester") && command_request!(selection: definition.fetch("selection"), peer: caller, deadline: deadline) == definition
107
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity, "command requester or policy changed"
108
- end
109
- true
110
- end
111
-
112
- def command_child!(definition:, child:, launcher:, deadline:)
113
- command_launcher!(peer: launcher, deadline: deadline)
114
- unless command_request!(selection: definition.fetch("selection"), peer: definition.fetch("requester"), deadline: deadline) == definition
115
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity, "command admission changed before exec"
116
- end
117
- # The authenticated root daemon witnesses cwd/identity after dropping
118
- # the gated child. Owner independently captures its kernel generation.
119
- keys = %w[pid uid gid groups started_at host cwd process_group no_new_privs capabilities]
120
- actual = Ace::Runtime::Molecules::ProcessIdentity.new(executor:
121
- Ace::Herdr::Molecules::BoundedObservation.new(deadline: deadline)).capture(child["pid"])
122
- executor = definition.fetch("executor")
123
- unless child.is_a?(Hash) && child.keys.sort == keys.sort && actual &&
124
- actual == child.slice("pid", "uid", "started_at", "host") &&
125
- child["no_new_privs"] == 1 && child["capabilities"].is_a?(Hash) &&
126
- child["capabilities"].keys.sort == %w[CapAmb CapEff CapInh CapPrm] &&
127
- child["capabilities"].values.all? { |value| value.is_a?(String) && value.match?(/\A0{16}\z/) } &&
128
- child.values_at("uid", "gid", "groups", "cwd", "process_group") ==
129
- [executor.fetch("uid"), executor.fetch("gid"), executor.fetch("groups"), executor.fetch("cwd"), child.fetch("pid")]
130
- raise Ace::Assign::AttemptErrors::UnauthorizedIdentity, "gated command child differs"
131
- end
132
- {"kernel_identity" => actual, "root_witness" => child, "witnessed_by" => launcher}
133
- end
134
-
135
- private
136
-
137
- def policy = @policy_loader.call
138
-
139
- def verify_context!(definition, deadline)
140
- executor = definition.fetch("executor")
141
- cwd = executor.fetch("cwd")
142
- wire = Ace::Runtime::Molecules::ProtectedSocket
143
- wire.root_path!(cwd, directory: true)
144
- executor.fetch("path").each { |entry| GrantResolver.trusted_directory!(entry) }
145
- home = executor.fetch("home")
146
- GrantResolver.trusted_directory!(home) if File.exist?(home) || File.symlink?(home)
147
- count = 0
148
- Find.find(cwd) do |path|
149
- raise InvalidConfigurationError, "command source exceeds verification bound" if (count += 1) > 20_000 || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
150
- stat = File.lstat(path)
151
- unless stat.uid.zero? && (stat.mode & 0o022).zero? && !stat.symlink? && (stat.file? || stat.directory?)
152
- raise InvalidConfigurationError, "command source must be an immutable installed view"
153
- end
154
- end
155
- executable = File.expand_path(definition.fetch("argv").first, cwd)
156
- raise InvalidConfigurationError, "command executable exceeds verification bound" if File.size(executable) > 64 * 1024 * 1024
157
- bytes = GrantResolver.read_verified(executable)
158
- unless bytes && File.executable?(executable) && Digest::SHA256.hexdigest(bytes) == definition.fetch("executable_sha256")
159
- raise InvalidConfigurationError, "command executable differs"
160
- end
161
- source = definition.fetch("source")
162
- if source.fetch("head")
163
- result = Ace::Herdr::Molecules::BoundedProcess.call(["/usr/bin/git", "-c", "safe.directory=#{cwd}",
164
- "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "rev-parse", "HEAD", "HEAD^{tree}"],
165
- environment: Ace::Assign::Molecules::InstalledJournalRead::ENVIRONMENT, chdir: cwd,
166
- stdin_data: "", timeout_s: deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), output_limit: 1024, cleanup_group: true)
167
- unless result.status.success? && result.stdout.lines.map(&:strip) == source.values_at("head", "tree")
168
- raise InvalidConfigurationError, "command accepted source differs"
169
- end
170
- clean = Ace::Herdr::Molecules::BoundedProcess.call(["/usr/bin/git", "-c", "safe.directory=#{cwd}",
171
- "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "status", "--porcelain=v1", "-z", "--untracked-files=all"],
172
- environment: Ace::Assign::Molecules::InstalledJournalRead::ENVIRONMENT, chdir: cwd,
173
- stdin_data: "", timeout_s: deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), output_limit: 65_536, cleanup_group: true)
174
- unless clean.status.success? && clean.stdout.empty? && !clean.oversized
175
- raise InvalidConfigurationError, "command accepted source is dirty"
176
- end
177
- end
178
- true
179
- end
180
- end
181
- end
182
- end
183
- end