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
|
@@ -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
|