ace-assign 0.64.0 → 0.66.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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
  3. data/CHANGELOG.md +25 -0
  4. data/docs/protected-authority.md +23 -7
  5. data/docs/usage.md +117 -4
  6. data/exe/ace-scope-ready +55 -0
  7. data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
  8. data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
  9. data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
  10. data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
  11. data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
  12. data/lib/ace/assign/authority/campaign_execution.rb +119 -0
  13. data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
  14. data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
  15. data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
  16. data/lib/ace/assign/authority/client.rb +156 -6
  17. data/lib/ace/assign/authority/deployment.rb +201 -39
  18. data/lib/ace/assign/authority/deployment_history.rb +146 -0
  19. data/lib/ace/assign/authority/endcap.rb +131 -18
  20. data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
  21. data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
  22. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
  23. data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
  24. data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
  25. data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
  26. data/lib/ace/assign/authority/endcap_publication.rb +207 -0
  27. data/lib/ace/assign/authority/endcap_results.rb +36 -18
  28. data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
  29. data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
  30. data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
  31. data/lib/ace/assign/authority/endcap_services.rb +221 -18
  32. data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
  33. data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
  34. data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
  35. data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
  36. data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
  37. data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
  38. data/lib/ace/assign/authority/launch_driver.rb +298 -19
  39. data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
  40. data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
  41. data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
  42. data/lib/ace/assign/authority/launch_review.rb +91 -0
  43. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
  44. data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
  45. data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
  46. data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
  47. data/lib/ace/assign/authority/launch_steering.rb +353 -0
  48. data/lib/ace/assign/authority/launch_stop.rb +254 -0
  49. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
  50. data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
  51. data/lib/ace/assign/authority/posix_acl.rb +5 -2
  52. data/lib/ace/assign/authority/prepared_input.rb +130 -0
  53. data/lib/ace/assign/authority/prepared_queue.rb +185 -0
  54. data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
  55. data/lib/ace/assign/authority/prepared_work.rb +304 -0
  56. data/lib/ace/assign/authority/prepared_worker.rb +69 -0
  57. data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
  58. data/lib/ace/assign/authority/readiness_hook.rb +120 -0
  59. data/lib/ace/assign/authority/router.rb +24 -2
  60. data/lib/ace/assign/authority/server.rb +103 -33
  61. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
  62. data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
  63. data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
  64. data/lib/ace/assign/authority/service_evidence.rb +243 -12
  65. data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
  66. data/lib/ace/assign/authority/task_context_entry.rb +32 -0
  67. data/lib/ace/assign/authority/transfer_codec.rb +44 -12
  68. data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
  69. data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
  70. data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
  71. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
  72. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
  73. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
  74. data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
  75. data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
  76. data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
  77. data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
  78. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
  79. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
  80. data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
  81. data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
  82. data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
  83. data/lib/ace/assign/cli/commands/create.rb +2 -0
  84. data/lib/ace/assign/cli/commands/delivery.rb +54 -4
  85. data/lib/ace/assign/cli/commands/fail.rb +6 -0
  86. data/lib/ace/assign/cli/commands/finish.rb +23 -2
  87. data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
  88. data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
  89. data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
  90. data/lib/ace/assign/cli/commands/resume.rb +12 -0
  91. data/lib/ace/assign/cli/commands/select.rb +2 -0
  92. data/lib/ace/assign/cli/commands/start.rb +2 -0
  93. data/lib/ace/assign/cli/commands/status.rb +4 -2
  94. data/lib/ace/assign/cli/commands/step.rb +3 -1
  95. data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
  96. data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
  97. data/lib/ace/assign/cli.rb +36 -5
  98. data/lib/ace/assign/errors.rb +139 -0
  99. data/lib/ace/assign/models/assignment.rb +26 -4
  100. data/lib/ace/assign/models/evidence_event.rb +3 -3
  101. data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
  102. data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
  103. data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
  104. data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
  105. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
  106. data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
  107. data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
  108. data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
  109. data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
  110. data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
  111. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
  112. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
  113. data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
  114. data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
  115. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
  116. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
  117. data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
  118. data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
  119. data/lib/ace/assign/molecules/step_writer.rb +27 -5
  120. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
  121. data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
  122. data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
  123. data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
  124. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
  125. data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
  126. data/lib/ace/assign/version.rb +1 -1
  127. data/lib/ace/assign.rb +1 -112
  128. metadata +90 -11
  129. data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "../../../authority/protected_assignment_context"
4
5
 
5
6
  module Ace
6
7
  module Assign
@@ -13,6 +14,54 @@ module Ace
13
14
  module Base
14
15
  private
15
16
 
17
+ def protected_context(options)
18
+ keys = %i[mapping assignment result head candidate_generation mutation expected_generation inbox_context registration signature]
19
+ keys.each do |key|
20
+ if options.key?(key) && options[key].to_s.strip.empty?
21
+ raise Ace::Support::Cli::Error, "Empty protected --#{key.to_s.tr('_', '-')} selector"
22
+ end
23
+ end
24
+ context = @protected_assignment_context ||= Ace::Assign::Authority::ProtectedAssignmentContext.load
25
+ selected = keys.any? do |key|
26
+ options.key?(key)
27
+ end
28
+ context if context.protected_participant? || selected || context.mapping_hint?
29
+ rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
30
+ protected_transport_unavailable!
31
+ end
32
+
33
+ def protected_attempt_request(context, options, usage)
34
+ params = {"assignment_id" => require_option(options, :assignment, usage),
35
+ "attempt_id" => require_option(options, :attempt, usage),
36
+ "expected_generation" => integer_option(options, :expected_generation, usage)}
37
+ context.verify_attempt_hints!(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"))
38
+ [context.client(options: options), params, require_option(options, :mutation, usage)]
39
+ rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
40
+ protected_transport_unavailable!
41
+ end
42
+
43
+ def protected_call(client, operation, params, mutation)
44
+ client.call(operation, params, mutation_id: mutation, timeout: 30).data
45
+ rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
46
+ protected_transport_unavailable!
47
+ end
48
+
49
+ def protected_transport_unavailable!
50
+ raise AttemptErrors::EvidenceUnavailable, "Protected authority outcome unavailable; retain original selectors and mutation for reconciliation"
51
+ end
52
+
53
+ def integer_option(options, key, usage, positive: false)
54
+ value = options[key]
55
+ unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A(?:0|[1-9][0-9]*)\z/))
56
+ raise Ace::Support::Cli::Error, "--#{key.to_s.tr('_', '-')} requires an exact integer: #{usage}"
57
+ end
58
+ number = value.is_a?(Integer) ? value : Integer(value, 10)
59
+ unless positive ? number.positive? : number >= 0
60
+ raise Ace::Support::Cli::Error, "--#{key.to_s.tr('_', '-')} is out of range"
61
+ end
62
+ number
63
+ end
64
+
16
65
  def build_coordinator
17
66
  Organisms::AttemptCoordinator.new
18
67
  end
@@ -8,18 +8,34 @@ module Ace
8
8
  module CLI
9
9
  module Commands
10
10
  module Attempt
11
- # Accept a structured execution receipt and record the outcome.
12
- # Terminal and uncertain attempts refuse new receipts.
11
+ # Protected completion selects an already submitted canonical result.
12
+ # Ordinary local completion accepts its structured receipt file.
13
13
  class Finish < Ace::Support::Cli::Command
14
14
  include Ace::Support::Cli::Base
15
15
  include Attempt::Base
16
16
 
17
- desc "Finish an attempt with a structured execution receipt"
17
+ desc "Finish a protected canonical result or an ordinary local receipt"
18
18
 
19
19
  option :attempt, desc: "Attempt ID"
20
- option :receipt, desc: "Path to the receipt JSON file"
20
+ option :mapping, desc: "Exact installed protected mapping ID"
21
+ option :assignment, desc: "Exact protected assignment ID"
22
+ option :mutation, desc: "Stable original authority mutation ID"
23
+ option :expected_generation, desc: "Original authority mutation generation"
24
+ option :receipt, desc: "Path to the ordinary local receipt JSON file"
25
+ option :result, desc: "Already submitted canonical protected result ID"
26
+ option :head, desc: "Exact canonical candidate head"
27
+ option :candidate_generation, desc: "Original canonical candidate generation"
21
28
 
22
29
  def call(**options)
30
+ if (context = protected_context(options))
31
+ raise Ace::Support::Cli::Error, "Protected finish forbids --receipt" unless options[:receipt].to_s.empty?
32
+ usage = "finish --mapping MAP --assignment ID --attempt ID --result ID --head SHA --candidate-generation N --mutation ID --expected-generation N"
33
+ client, params, mutation = protected_attempt_request(context, options, usage)
34
+ params.merge!("result_id" => require_option(options, :result, usage), "head" => require_option(options, :head, usage),
35
+ "candidate_generation" => integer_option(options, :candidate_generation, usage, positive: true))
36
+ emit_json(protected_call(client, "finish", params, mutation))
37
+ return
38
+ end
23
39
  attempt_id = require_option(options, :attempt, "finish --attempt ID --receipt FILE")
24
40
  receipt = require_option(options, :receipt, "finish --attempt ID --receipt FILE")
25
41
 
@@ -17,9 +17,20 @@ module Ace
17
17
  desc "Reconcile an interrupted or uncertain attempt"
18
18
 
19
19
  option :attempt, desc: "Attempt ID"
20
+ option :mapping, desc: "Exact installed protected mapping ID"
21
+ option :assignment, desc: "Exact protected assignment ID"
22
+ option :mutation, desc: "Stable original authority mutation ID"
23
+ option :expected_generation, desc: "Original authority mutation generation"
20
24
  option :receipt, desc: "Path to the receipt JSON file (required to resolve uncertainty)"
21
25
 
22
26
  def call(**options)
27
+ if (context = protected_context(options))
28
+ raise Ace::Support::Cli::Error, "Protected recovery forbids --receipt" unless options[:receipt].to_s.empty?
29
+ usage = "reconcile --mapping MAP --assignment ID --attempt ID --mutation ID --expected-generation N"
30
+ client, params, mutation = protected_attempt_request(context, options, usage)
31
+ emit_json(protected_call(client, "recover", params, mutation))
32
+ return
33
+ end
23
34
  attempt_id = require_option(options, :attempt, "reconcile --attempt ID [--receipt FILE]")
24
35
  receipt = options[:receipt].to_s.strip
25
36
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../../authority/prepared_task_context"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ module Authority
9
+ class InboxContextPrincipal < Ace::Support::Cli::Command
10
+ desc "Classify the actual caller through every retained protected owner"
11
+
12
+ def call(**options)
13
+ raise ArgumentError, "fixed inbox context options differ" unless options.empty?
14
+ consumer = Ace::Assign::Authority::PreparedTaskContext.new(context: @protected_assignment_context)
15
+ $stdout.write(consumer.inbox_principal())
16
+ 0
17
+ rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
18
+ raise Ace::Support::Cli::Error, error.message
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../../authority/prepared_task_context"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ module Authority
9
+ class InboxContextSelection < Ace::Support::Cli::Command
10
+ desc "Read the fixed installed inbox context selection"
11
+ option :project, required: true, desc: "Installed project ID"
12
+ option :mapping, required: true, desc: "Installed mapping ID"
13
+ option :inbox_context, required: true, desc: "Installed inbox context ID"
14
+
15
+ def call(**options)
16
+ raise ArgumentError, "fixed inbox context options differ" unless options.keys.sort == %i[inbox_context mapping project]
17
+ consumer = Ace::Assign::Authority::PreparedTaskContext.new(context: @protected_assignment_context)
18
+ $stdout.write(consumer.inbox_selection(**options))
19
+ 0
20
+ rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
21
+ raise Ace::Support::Cli::Error, error.message
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -7,10 +7,13 @@ module Ace
7
7
  module Commands
8
8
  module Authority
9
9
  class Launch < Ace::Support::Cli::Command
10
- desc "Launch one mapped native worker through the protected gate"
10
+ desc "Launch one mapped worker and retain its original foreground control channel"
11
+ READY_FIELDS = %w[assignment_id attempt_id generation journal_commit mapping_id original_binding_digest type version].freeze
12
+ READY_LIMIT = 16_384
11
13
  option :mapping, required: true, desc: "Installed launch mapping ID"
12
14
  option :assignment, desc: "Managed assignment ID"
13
- option :definition, desc: "Current managed assignment definition JSON file"
15
+ option :definition, desc: "Retained final prepared assignment definition JSON file"
16
+ option :prepared_bundle, desc: "Retained original prepared input Git bundle"
14
17
  option :step, desc: "Assignment subtree scope"
15
18
  option :base_head, desc: "Exact base commit SHA"
16
19
  option :mutation, desc: "Stable invocation ID for safe replay"
@@ -20,20 +23,69 @@ module Ace
20
23
  result = if options[:dry_run]
21
24
  driver.preflight
22
25
  else
23
- %i[assignment definition step base_head].each do |key|
26
+ %i[assignment definition prepared_bundle step base_head].each do |key|
24
27
  raise ArgumentError, "Missing --#{key.to_s.tr('_', '-')}" unless options[key].is_a?(String) && !options[key].empty?
25
28
  end
26
- raise ArgumentError, "assignment definition is oversized" if File.size(options[:definition]) > 32_768
27
- args = {assignment_id: options[:assignment], definition_bytes: File.read(options[:definition]),
29
+ definition = retained_input(options[:definition], 32_768).force_encoding(Encoding::UTF_8)
30
+ raise ArgumentError, "assignment definition is not UTF-8" unless definition.valid_encoding?
31
+ bundle = retained_input(options[:prepared_bundle], Ace::Assign::Authority::CandidateTransfer::MAX_BYTES)
32
+ args = {prepared_bundle: bundle, assignment_id: options[:assignment], definition_bytes: definition,
28
33
  scope: options[:step], base_head: options[:base_head]}
29
34
  args[:mutation_id] = options[:mutation] if options[:mutation]
30
35
  driver.launch(**args)
31
36
  end
32
- puts JSON.generate(result)
37
+ if !options[:dry_run] && result["phase"] == "issued"
38
+ retaining_control = true
39
+ announced = false
40
+ driver.serve_control!(state: result) do |ready|
41
+ raise ArgumentError, "original launch readiness was already announced" if announced
42
+ line = readiness_line(ready, result, options)
43
+ $stdout.write(line)
44
+ $stdout.flush
45
+ announced = true
46
+ end
47
+ raise ArgumentError, "original control ended before authenticated readiness" unless announced
48
+ else
49
+ puts JSON.generate(result)
50
+ end
33
51
  rescue ArgumentError, SystemCallError, Ace::Runtime::Error, Ace::Assign::Error => error
34
52
  raise Ace::Support::Cli::Error, error.message
53
+ ensure
54
+ # Local foreground-loop cancellation is not a stopped/settled proof.
55
+ driver.request_control_cancel if retaining_control
35
56
  end
36
57
  private
58
+ def retained_input(path, limit)
59
+ File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
60
+ before = file.stat
61
+ unless before.file? && before.size.between?(1, limit)
62
+ raise ArgumentError, "retained launch input is not a bounded regular file"
63
+ end
64
+ bytes = (file.read(limit + 1) || "").b
65
+ after = file.stat
66
+ identity = ->(stat) { [stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime] }
67
+ unless bytes.bytesize == before.size && bytes.bytesize <= limit && identity.call(before) == identity.call(after)
68
+ raise ArgumentError, "retained launch input changed while reading"
69
+ end
70
+ bytes
71
+ end
72
+ end
73
+
74
+ def readiness_line(ready, result, options)
75
+ unless ready.is_a?(Hash) && ready.keys.all? { |key| key.is_a?(String) } && ready.keys.sort == READY_FIELDS &&
76
+ ready["version"].is_a?(Integer) && ready["version"] == 1 && ready["type"] == "launch_ready" &&
77
+ ready.values_at("mapping_id", "assignment_id", "attempt_id") ==
78
+ [options.fetch(:mapping), options.fetch(:assignment), result.fetch("attempt_id")] &&
79
+ ready["generation"].is_a?(Integer) && ready["generation"].positive? &&
80
+ ready["journal_commit"].is_a?(String) && ready["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
81
+ ready["original_binding_digest"].is_a?(String) && ready["original_binding_digest"].match?(/\A[0-9a-f]{64}\z/)
82
+ raise ArgumentError, "original launch readiness is malformed or mismatched"
83
+ end
84
+ line = JSON.generate(ready) + "\n"
85
+ raise ArgumentError, "original launch readiness is oversized" if line.bytesize > READY_LIMIT
86
+ line
87
+ end
88
+
37
89
  def build_driver(mapping)
38
90
  Ace::Assign::Authority::LaunchDriver.new(mapping_id: mapping)
39
91
  end
@@ -12,7 +12,8 @@ module Ace
12
12
  option :authority, required: true, desc: "Installed authority ID"
13
13
  def call(**options)
14
14
  deployment = Ace::Assign::Authority::Deployment.load
15
- launch = Ace::Assign::Authority::LaunchLifecycle.new(deployment: deployment)
15
+ history = Ace::Assign::Authority::DeploymentHistory.load
16
+ launch = Ace::Assign::Authority::LaunchLifecycle.new(deployment: deployment, deployment_history: history)
16
17
  router = Ace::Assign::Authority::Router.new(launch: launch)
17
18
  server = Ace::Assign::Authority::Server.new(authority_id: options.fetch(:authority), deployment: deployment, lifecycle: router)
18
19
  signals = %w[INT TERM].to_h { |signal| [signal, Signal.trap(signal) { server.request_stop }] }
@@ -13,7 +13,7 @@ module Ace
13
13
  option :attempt, required: true, desc: "Attempt ID"
14
14
  def call(**options)
15
15
  reply = Ace::Assign::Authority::Client.new(mapping_id: options.fetch(:mapping)).call("attempt_status",
16
- {"assignment_id" => options.fetch(:assignment), "attempt_id" => options.fetch(:attempt)})
16
+ {"assignment_id" => options.fetch(:assignment), "attempt_id" => options.fetch(:attempt), "result_candidate_generation" => nil})
17
17
  puts JSON.generate(reply.data)
18
18
  rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
19
19
  raise Ace::Support::Cli::Error, error.message
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../../authority/prepared_task_context"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ module Authority
9
+ class TaskContext < Ace::Support::Cli::Command
10
+ desc "Return the captured original task context through the fixed selected entry"
11
+ option :mapping, required: true, desc: "Installed original mapping ID"
12
+ option :assignment, required: true, desc: "Original assignment ID@SCOPE"
13
+ option :attempt, required: true, desc: "Original protected attempt ID"
14
+ option :task, required: true, desc: "Captured canonical task ID"
15
+
16
+ def call(**options)
17
+ unless options.keys.sort == %i[assignment attempt mapping task]
18
+ raise ArgumentError, "fixed task-context accepts only its four selectors"
19
+ end
20
+ consumer = Ace::Assign::Authority::PreparedTaskContext.new(context: @protected_assignment_context)
21
+ $stdout.write(consumer.call(**options))
22
+ 0
23
+ rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
24
+ raise Ace::Support::Cli::Error, error.message
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../../authority/prepared_task_context"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ module Authority
9
+ class TaskContextPrincipal < Ace::Support::Cli::Command
10
+ desc "Classify this actual caller through the installed and retained protected owners"
11
+
12
+ def call(**options)
13
+ raise ArgumentError, "fixed task-context-principal accepts no options" unless options.empty?
14
+ consumer = Ace::Assign::Authority::PreparedTaskContext.new(context: @protected_assignment_context)
15
+ $stdout.write(consumer.principal)
16
+ 0
17
+ rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
18
+ raise Ace::Support::Cli::Error, error.message
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../../authority/prepared_task_context"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ module Authority
9
+ class TaskContextSelection < Ace::Support::Cli::Command
10
+ desc "Return the authenticated original task-context entry without captured text"
11
+ option :mapping, required: true, desc: "Installed original mapping ID"
12
+ option :assignment, required: true, desc: "Original assignment ID@SCOPE"
13
+ option :attempt, required: true, desc: "Original protected attempt ID"
14
+
15
+ def call(**options)
16
+ unless options.keys.sort == %i[assignment attempt mapping]
17
+ raise ArgumentError, "fixed task-context-selection accepts only its three selectors"
18
+ end
19
+ consumer = Ace::Assign::Authority::PreparedTaskContext.new(context: @protected_assignment_context)
20
+ $stdout.write(consumer.selection(**options))
21
+ 0
22
+ rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
23
+ raise Ace::Support::Cli::Error, error.message
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../../authority/prepared_worker"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ module Authority
9
+ class Worker < Ace::Support::Cli::Command
10
+ desc "Consume the original authenticated prepared subtree as its fixed worker"
11
+
12
+ def call(**options)
13
+ raise ArgumentError, "fixed worker accepts no options" unless options.empty?
14
+ Ace::Assign::Authority::PreparedWorker.new.run
15
+ rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
16
+ raise Ace::Support::Cli::Error, error.message
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ require_relative "protected_submission"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ class CampaignExportResult < Ace::Support::Cli::Command
9
+ include Ace::Support::Cli::Base
10
+ include ProtectedSubmission
11
+ desc "Download a verified accepted campaign result through its original authority"
12
+ option :mapping, desc: "Original installed parent mapping"
13
+ option :assignment, desc: "Original parent assignment"
14
+ option :attempt, desc: "Original parent attempt"
15
+ option :head, desc: "Exact parent candidate head"
16
+ option :candidate_generation, desc: "Exact parent candidate counter"
17
+ option :output, desc: "Result byte file (created once, exact retries allowed)"
18
+
19
+ def call(**options)
20
+ usage = "campaign-export-result with original parent selectors and --output FILE"
21
+ context = protected_context(options)
22
+ raise AttemptErrors::EvidenceUnavailable, "Campaign export requires installed authority" unless context
23
+ params = {"assignment_id" => require_option(options, :assignment, usage),
24
+ "attempt_id" => require_option(options, :attempt, usage), "head" => require_option(options, :head, usage),
25
+ "candidate_generation" => integer_option(options, :candidate_generation, usage, positive: true)}
26
+ raise Ace::Support::Cli::Error, "Campaign head must be an exact SHA-1" unless params["head"].match?(/\A[0-9a-f]{40}\z/)
27
+ context.verify_attempt_hints!(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"))
28
+ path = require_option(options, :output, usage)
29
+ reply = context.client(options: options).call("campaign_export_result", params, download: true, purpose: :artifacts, timeout: 30)
30
+ bytes = reply.parts&.first
31
+ unless reply.parts&.size == 1 && bytes.is_a?(String) && bytes.bytesize <= 64 * 1024 &&
32
+ reply.data.values_at("head", "candidate_generation", "bytes", "sha256") ==
33
+ [params.fetch("head"), params.fetch("candidate_generation"), bytes.bytesize, Digest::SHA256.hexdigest(bytes)]
34
+ raise AttemptErrors::EvidenceUnavailable, "Campaign result download differs"
35
+ end
36
+ begin
37
+ File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
38
+ file.write(bytes)
39
+ file.flush
40
+ file.fsync
41
+ end
42
+ rescue Errno::EEXIST
43
+ raise Ace::Support::Cli::Error, "Existing campaign result file differs" unless input_bytes(path, limit: 64 * 1024) == bytes
44
+ end
45
+ emit_json(reply.data.except("transfer").merge("path" => path))
46
+ rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
47
+ protected_transport_unavailable!
48
+ rescue SystemCallError, IOError
49
+ raise Ace::Support::Cli::Error, "Campaign result file or transport is unavailable"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ require_relative "protected_submission"
3
+ require_relative "../../authority/campaign_round_transfer"
4
+
5
+ module Ace
6
+ module Assign
7
+ module CLI
8
+ module Commands
9
+ class CampaignRecordRound < Ace::Support::Cli::Command
10
+ include Ace::Support::Cli::Base
11
+ include ProtectedSubmission
12
+ desc "Consume settled protected campaign children into the original R1 store"
13
+ option :mapping, desc: "Original installed parent mapping"
14
+ option :assignment, desc: "Original parent assignment"
15
+ option :attempt, desc: "Original parent attempt"
16
+ option :head, desc: "Exact parent candidate head"
17
+ option :candidate_generation, desc: "Exact parent candidate counter"
18
+ option :mutation, desc: "Stable R1 round attempt ID"
19
+ option :input, desc: "Closed bounded round envelope"
20
+ option :artifact, type: :array, desc: "Ordered report byte files"
21
+
22
+ def call(**options)
23
+ usage = "campaign-record-round with original parent selectors, --mutation ID and --input FILE"
24
+ context = protected_context(options)
25
+ raise AttemptErrors::EvidenceUnavailable, "Campaign consumption requires installed authority" unless context
26
+ params = {"assignment_id" => require_option(options, :assignment, usage),
27
+ "attempt_id" => require_option(options, :attempt, usage), "head" => require_option(options, :head, usage),
28
+ "candidate_generation" => integer_option(options, :candidate_generation, usage, positive: true)}
29
+ unless params["head"].match?(/\A[0-9a-f]{40}\z/)
30
+ raise Ace::Support::Cli::Error, "Campaign head must be an exact SHA-1"
31
+ end
32
+ context.verify_attempt_hints!(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"))
33
+ mutation = require_option(options, :mutation, usage)
34
+ bytes = input_bytes(require_option(options, :input, usage), limit: 16 * 1024)
35
+ files = options.fetch(:artifact, [])
36
+ raise Ace::Support::Cli::Error, "Campaign artifacts require at most 16 files" unless files.is_a?(Array) && files.size <= 16
37
+ parts = [bytes, *files.map { |path| input_bytes(path, limit: 64 * 1024, nonempty: false) }]
38
+ params["input_sha256"] = Digest::SHA256.hexdigest(bytes)
39
+ input = Struct.new(:parts) do
40
+ def count = parts.length
41
+ def bytes(index:) = parts.fetch(index)
42
+ end.new(parts)
43
+ admitted = Ace::Assign::Authority::CampaignRoundTransfer.decode(input: input, sha256: params.fetch("input_sha256"))
44
+ unless admitted.fetch(:round).values_at("attempt_id", "head") == [mutation, params.fetch("head")]
45
+ raise Ace::Support::Cli::Error, "Campaign input request ID or head differs"
46
+ end
47
+ emit_json(upload_call(context.client(options: options), "campaign_record_round", params, mutation, parts, :receipt_artifacts))
48
+ rescue SecurityError, Ace::Runtime::RuntimeUnavailableError
49
+ protected_transport_unavailable!
50
+ rescue ArgumentError => error
51
+ raise Ace::Support::Cli::Error, error.message
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,6 +8,7 @@ module Ace
8
8
  # Create a new workflow assignment from YAML or preset-backed task refs
9
9
  class Create < Ace::Support::Cli::Command
10
10
  include Ace::Support::Cli::Base
11
+ include AssignmentTarget
11
12
 
12
13
  desc "Create a new workflow assignment"
13
14
 
@@ -20,6 +21,7 @@ module Ace
20
21
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
21
22
 
22
23
  def call(yaml: nil, task: nil, preset: nil, **options)
24
+ refuse_protected_graph_mutation!(options)
23
25
  validate_modes!(yaml, task, preset)
24
26
  raise Ace::Support::Cli::Error, "--delivery-parameters requires --task" if yaml && options[:delivery_parameters]
25
27
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
+ require_relative "../../authority/protected_assignment_context"
4
+ require_relative "../../organisms/protected_delivery_coordinator"
3
5
 
4
6
  module Ace
5
7
  module Assign
@@ -15,17 +17,53 @@ module Ace
15
17
  option :body_file, desc: "PR description file"
16
18
  option :tests, desc: "JSON reference to coordinator-accepted test attempt and receipt digest"
17
19
  option :review, desc: "JSON reference to coordinator-accepted independent review attempt and receipt digest"
18
- option :service_request, desc: "Completed authorized qjx merge request ID (merge consumption only)"
20
+ option :service_request, desc: "Original authorized PR service request ID"
21
+ option :mapping, desc: "Installed original protected mapping selector"
22
+ option :scope, desc: "Original prepared worker scope"
23
+ option :candidate_head, desc: "Exact original protected candidate SHA"
24
+ option :candidate_generation, type: :integer, desc: "Exact original protected candidate generation"
25
+ option :input_digest, desc: "Exact original accepted service input SHA256"
26
+ option :target, desc: "Exact original target repository or PR URL"
27
+ option :artifact_digest, desc: "Original target artifact SHA256, if present"
28
+
29
+ def initialize(protected_context: nil)
30
+ @protected_assignment_context = protected_context
31
+ end
19
32
 
20
33
  def call(**options)
34
+ context = @protected_assignment_context ||= Ace::Assign::Authority::ProtectedAssignmentContext.load
35
+ protected = context.protected_participant? || options[:mapping] || context.mapping_hint?
36
+ if protected
37
+ unless context.protected_worker?
38
+ raise AttemptErrors::EvidenceUnavailable, "protected delivery is worker-only"
39
+ end
40
+ unless %w[create update ready merge status].include?(options[:operation]) && %i[title body_file tests review].all? { |key| options[key].nil? }
41
+ raise ArgumentError, "protected delivery accepts canonical PR results/status only; local input/evidence flags are unavailable"
42
+ end
43
+ input = context.resolve(options: options, assignment_id: options[:assignment], scope: options[:scope])
44
+ raise AttemptErrors::EvidenceUnavailable, "original protected prepared input is unavailable" unless input
45
+ result = Organisms::ProtectedDeliveryCoordinator.new(client: context.client(options: options),
46
+ project_id: input.work.manifest.fetch("project_id")).perform(assignment_id: options[:assignment],
47
+ attempt_id: options[:attempt], operation: options[:operation], service_request_id: options[:service_request],
48
+ candidate_head: options[:candidate_head], candidate_generation: options[:candidate_generation],
49
+ input_digest: options[:input_digest], target: {"resource" => options[:target], "artifact_digest" => options[:artifact_digest]},
50
+ parameters: read_json(options[:parameters]))
51
+ puts JSON.generate(json_value(result))
52
+ return
53
+ end
54
+ if %i[mapping scope candidate_head candidate_generation input_digest target artifact_digest].any? { |key| options[key] }
55
+ raise ArgumentError, "protected delivery selectors cannot select ordinary local delivery"
56
+ end
21
57
  result = build_delivery.perform(assignment_id: options[:assignment], attempt_id: options[:attempt],
22
58
  operation: options[:operation], parameters: read_json(options[:parameters]),
23
59
  title: options[:title], body: options[:body_file] && File.read(options[:body_file]),
24
60
  tests: read_json(options[:tests]), review: read_json(options[:review]),
25
61
  service_request_id: options[:service_request])
26
62
  puts JSON.generate(json_value(result))
27
- rescue ArgumentError, Ace::Git::Error, Ace::Assign::Error, Errno::ENOENT => e
28
- raise Ace::Support::Cli::Error, e.message
63
+ rescue ArgumentError, Ace::Git::Error, Ace::Assign::Error, Ace::Runtime::RuntimeUnavailableError,
64
+ JSON::ParserError, SecurityError, SystemCallError => e
65
+ message = e.is_a?(JSON::ParserError) ? "protected installed selection unavailable" : e.message
66
+ raise Ace::Support::Cli::Error, message
29
67
  end
30
68
 
31
69
  private
@@ -36,7 +74,19 @@ module Ace
36
74
 
37
75
  def read_json(path)
38
76
  return nil unless path
39
- data = JSON.parse(File.read(path))
77
+ bytes = File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
78
+ before = file.stat
79
+ raise ArgumentError, "Delivery input must be a bounded regular file" unless before.file? && before.size <= 65_536
80
+ value = file.read(65_537)
81
+ after = file.stat
82
+ unless value && value.bytesize <= 65_536 &&
83
+ [before.dev, before.ino, before.size, before.mtime, before.ctime] == [after.dev, after.ino, after.size, after.mtime, after.ctime]
84
+ raise ArgumentError, "Delivery input changed or exceeds its bound"
85
+ end
86
+ value
87
+ end
88
+ data = JSON.parse(bytes, max_nesting: 16, create_additions: false,
89
+ allow_duplicate_key: false, allow_comments: false, allow_nan: false)
40
90
  raise ArgumentError, "Delivery input must be a JSON object" unless data.is_a?(Hash)
41
91
  data
42
92
  rescue JSON::ParserError
@@ -12,6 +12,8 @@ module Ace
12
12
  desc "Mark current step as failed"
13
13
 
14
14
  option :message, aliases: ["-m"], required: true, desc: "Error message"
15
+ option :mapping, desc: "Installed protected launch mapping ID"
16
+ option :attempt, desc: "Original protected attempt ID"
15
17
  option :assignment, desc: "Target specific assignment ID"
16
18
  option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
17
19
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
@@ -28,6 +30,10 @@ module Ace
28
30
  puts "Step #{failed.number} (#{failed.name}) marked as failed"
29
31
  puts "Updated: #{File.basename(failed.file_path)}"
30
32
  puts "Error: #{message}"
33
+ if target.prepared_input
34
+ puts "Further work requires a reviewed new prepared version and attempt."
35
+ return
36
+ end
31
37
  puts
32
38
  puts "Options:"
33
39
  puts "- ace-assign add \"fix-step\" to add a fix step"