ace-assign 0.63.1 → 0.66.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  7. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  8. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  9. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  10. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  11. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  12. data/CHANGELOG.md +62 -0
  13. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  14. data/docs/protected-authority.md +282 -0
  15. data/docs/usage.md +205 -3
  16. data/exe/ace-scope-ready +55 -0
  17. data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
  18. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  19. data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
  20. data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
  21. data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
  22. data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
  23. data/lib/ace/assign/authority/campaign_execution.rb +119 -0
  24. data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
  25. data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
  26. data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
  27. data/lib/ace/assign/authority/client.rb +257 -0
  28. data/lib/ace/assign/authority/deployment.rb +627 -0
  29. data/lib/ace/assign/authority/deployment_history.rb +146 -0
  30. data/lib/ace/assign/authority/endcap.rb +455 -0
  31. data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
  32. data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
  33. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
  34. data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
  35. data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
  36. data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
  37. data/lib/ace/assign/authority/endcap_publication.rb +207 -0
  38. data/lib/ace/assign/authority/endcap_results.rb +398 -0
  39. data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
  40. data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
  41. data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
  42. data/lib/ace/assign/authority/endcap_services.rb +598 -0
  43. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  44. data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
  45. data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
  46. data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
  47. data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
  48. data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
  49. data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
  50. data/lib/ace/assign/authority/launch_driver.rb +393 -0
  51. data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
  52. data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
  53. data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
  54. data/lib/ace/assign/authority/launch_review.rb +91 -0
  55. data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
  56. data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
  57. data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
  58. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  59. data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
  60. data/lib/ace/assign/authority/launch_steering.rb +353 -0
  61. data/lib/ace/assign/authority/launch_stop.rb +254 -0
  62. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
  63. data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
  64. data/lib/ace/assign/authority/posix_acl.rb +59 -0
  65. data/lib/ace/assign/authority/prepared_input.rb +130 -0
  66. data/lib/ace/assign/authority/prepared_queue.rb +185 -0
  67. data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
  68. data/lib/ace/assign/authority/prepared_work.rb +304 -0
  69. data/lib/ace/assign/authority/prepared_worker.rb +69 -0
  70. data/lib/ace/assign/authority/private_directory.rb +33 -0
  71. data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
  72. data/lib/ace/assign/authority/readiness_hook.rb +120 -0
  73. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  74. data/lib/ace/assign/authority/router.rb +76 -0
  75. data/lib/ace/assign/authority/server.rb +300 -0
  76. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
  77. data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
  78. data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
  79. data/lib/ace/assign/authority/service_evidence.rb +286 -0
  80. data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
  81. data/lib/ace/assign/authority/task_context_entry.rb +32 -0
  82. data/lib/ace/assign/authority/transfer_codec.rb +187 -0
  83. data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
  84. data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
  85. data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
  86. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
  87. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
  88. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
  89. data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
  90. data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
  91. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  92. data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
  93. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
  94. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
  95. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  96. data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
  97. data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
  98. data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
  99. data/lib/ace/assign/cli/commands/create.rb +11 -1
  100. data/lib/ace/assign/cli/commands/delivery.rb +107 -0
  101. data/lib/ace/assign/cli/commands/fail.rb +6 -0
  102. data/lib/ace/assign/cli/commands/finish.rb +23 -2
  103. data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
  104. data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
  105. data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
  106. data/lib/ace/assign/cli/commands/resume.rb +38 -0
  107. data/lib/ace/assign/cli/commands/select.rb +2 -0
  108. data/lib/ace/assign/cli/commands/start.rb +2 -0
  109. data/lib/ace/assign/cli/commands/status.rb +4 -2
  110. data/lib/ace/assign/cli/commands/step.rb +3 -1
  111. data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
  112. data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
  113. data/lib/ace/assign/cli.rb +56 -4
  114. data/lib/ace/assign/errors.rb +139 -0
  115. data/lib/ace/assign/models/assignment.rb +26 -4
  116. data/lib/ace/assign/models/evidence_event.rb +4 -2
  117. data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
  118. data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
  119. data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
  120. data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
  121. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
  122. data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
  123. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  124. data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
  125. data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
  126. data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
  127. data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
  128. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
  129. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
  130. data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
  131. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  132. data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
  133. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
  134. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
  135. data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
  136. data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
  137. data/lib/ace/assign/molecules/step_writer.rb +27 -5
  138. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
  139. data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
  140. data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
  141. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  142. data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
  143. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
  144. data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
  145. data/lib/ace/assign/version.rb +1 -1
  146. data/lib/ace/assign.rb +4 -102
  147. metadata +186 -8
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+ require_relative "../../../authority/launch_driver"
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ module Authority
9
+ class Terminate < Ace::Support::Cli::Command
10
+ desc "Close the original gated native child and request proven abort"
11
+ option :mapping, required: true, desc: "Installed launch mapping ID"
12
+ option :assignment, required: true, desc: "Assignment ID"
13
+ option :attempt, required: true, desc: "Attempt ID"
14
+ def call(**options)
15
+ client = Ace::Assign::Authority::Client.new(mapping_id: options.fetch(:mapping))
16
+ state = client.call("inspect_launch", {"assignment_id" => options.fetch(:assignment), "attempt_id" => options.fetch(:attempt)}).data
17
+ binding = state.fetch("process_binding") { raise ArgumentError, "no positively recorded original child; supervisor inspection required" }
18
+ result = Ace::Assign::Authority::LaunchDriver.new(mapping_id: options.fetch(:mapping)).terminate(
19
+ state: state, binding: binding, evidence: "protected exact-child termination requested")
20
+ puts JSON.generate(result)
21
+ rescue ArgumentError, Ace::Runtime::Error, Ace::Assign::Error => error
22
+ raise Ace::Support::Cli::Error, error.message
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ 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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "json"
2
3
 
3
4
  module Ace
4
5
  module Assign
@@ -7,9 +8,11 @@ module Ace
7
8
  # Create a new workflow assignment from YAML or preset-backed task refs
8
9
  class Create < Ace::Support::Cli::Command
9
10
  include Ace::Support::Cli::Base
11
+ include AssignmentTarget
10
12
 
11
13
  desc "Create a new workflow assignment"
12
14
 
15
+ option :delivery_parameters, desc: "JSON file with forge selection and explicit PR provenance (task mode)"
13
16
  option :yaml, desc: "Path to job.yaml config file"
14
17
  option :task, aliases: ["-t"], type: :array,
15
18
  desc: "Task reference(s), repeatable and comma-separated"
@@ -18,14 +21,17 @@ module Ace
18
21
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
19
22
 
20
23
  def call(yaml: nil, task: nil, preset: nil, **options)
24
+ refuse_protected_graph_mutation!(options)
21
25
  validate_modes!(yaml, task, preset)
26
+ raise Ace::Support::Cli::Error, "--delivery-parameters requires --task" if yaml && options[:delivery_parameters]
22
27
 
23
28
  result = if yaml
24
29
  Organisms::AssignmentExecutor.new.start(yaml)
25
30
  else
26
31
  Organisms::TaskAssignmentCreator.new.call(
27
32
  task_refs: task,
28
- preset_name: preset || Organisms::TaskAssignmentCreator::DEFAULT_PRESET
33
+ preset_name: preset || Organisms::TaskAssignmentCreator::DEFAULT_PRESET,
34
+ delivery_parameters: options[:delivery_parameters] && JSON.parse(File.read(options[:delivery_parameters]))
29
35
  )
30
36
  end
31
37
 
@@ -34,6 +40,10 @@ module Ace
34
40
  print_assignment_header(result[:assignment])
35
41
  print_step_instructions(result[:current] || result[:state]&.next_workable)
36
42
  end
43
+ rescue JSON::ParserError, Errno::ENOENT
44
+ raise Ace::Support::Cli::Error, "Delivery parameters file unavailable or invalid JSON"
45
+ rescue ArgumentError => e
46
+ raise Ace::Support::Cli::Error, e.message
37
47
  end
38
48
 
39
49
  private
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+ require_relative "../../authority/protected_assignment_context"
4
+ require_relative "../../organisms/protected_delivery_coordinator"
5
+
6
+ module Ace
7
+ module Assign
8
+ module CLI
9
+ module Commands
10
+ class Delivery < Ace::Support::Cli::Command
11
+ desc "Execute or reconcile attempt-bound forge delivery"
12
+ option :assignment, required: true, desc: "Managed assignment ID"
13
+ option :attempt, required: true, desc: "Active delivery attempt ID"
14
+ option :operation, required: true, desc: "create, update, ready, review, merge, status"
15
+ option :parameters, desc: "JSON delivery parameters (otherwise assignment source delivery mapping)"
16
+ option :title, desc: "PR title"
17
+ option :body_file, desc: "PR description file"
18
+ option :tests, desc: "JSON reference to coordinator-accepted test attempt and receipt digest"
19
+ option :review, desc: "JSON reference to coordinator-accepted independent review attempt and receipt digest"
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
32
+
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
57
+ result = build_delivery.perform(assignment_id: options[:assignment], attempt_id: options[:attempt],
58
+ operation: options[:operation], parameters: read_json(options[:parameters]),
59
+ title: options[:title], body: options[:body_file] && File.read(options[:body_file]),
60
+ tests: read_json(options[:tests]), review: read_json(options[:review]),
61
+ service_request_id: options[:service_request])
62
+ puts JSON.generate(json_value(result))
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
67
+ end
68
+
69
+ private
70
+
71
+ def build_delivery
72
+ Organisms::DeliveryCoordinator.new(repo_root: Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current)
73
+ end
74
+
75
+ def read_json(path)
76
+ return nil unless 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)
90
+ raise ArgumentError, "Delivery input must be a JSON object" unless data.is_a?(Hash)
91
+ data
92
+ rescue JSON::ParserError
93
+ raise ArgumentError, "Delivery input must be valid JSON"
94
+ end
95
+
96
+ def json_value(value)
97
+ case value
98
+ when Hash then value.transform_values { |item| json_value(item) }
99
+ when Array then value.map { |item| json_value(item) }
100
+ else value.respond_to?(:to_h) ? json_value(value.to_h) : value
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -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"
@@ -13,6 +13,8 @@ module Ace
13
13
 
14
14
  argument :step, required: false, desc: "Step number to finish (active assignment only)"
15
15
  option :message, aliases: ["-m"], desc: "Report content: string, file path, or pipe stdin"
16
+ option :mapping, desc: "Installed protected launch mapping ID"
17
+ option :attempt, desc: "Original protected attempt ID"
16
18
  option :assignment, desc: "Target specific assignment ID"
17
19
  option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
18
20
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
@@ -24,7 +26,7 @@ module Ace
24
26
 
25
27
  target = resolve_assignment_target(options)
26
28
  executor = build_executor_for_target(target)
27
- report_content = resolve_report_content(options)
29
+ report_content = resolve_report_content(options, protected: !target.prepared_input.nil?)
28
30
  result = executor.finish_step(
29
31
  report_content: report_content,
30
32
  step_number: step,
@@ -72,8 +74,27 @@ module Ace
72
74
 
73
75
  private
74
76
 
75
- def resolve_report_content(options)
77
+ def resolve_report_content(options, protected: false)
76
78
  message = options[:message]&.strip
79
+ if protected
80
+ limit = Ace::Assign::Authority::PreparedWork::MAX_TEXT
81
+ if message && !message.empty? && File.exist?(message)
82
+ return File.open(message, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
83
+ before = file.stat
84
+ raise Error, "Report input must be a bounded regular file" unless before.file? && before.size.between?(1, limit)
85
+ bytes = file.read(limit + 1)
86
+ after = file.stat
87
+ unless bytes.bytesize == before.size && [before.dev, before.ino, before.size, before.mtime, before.ctime] ==
88
+ [after.dev, after.ino, after.size, after.mtime, after.ctime]
89
+ raise Error, "Report input changed"
90
+ end
91
+ bytes
92
+ end
93
+ end
94
+ bytes = message && !message.empty? ? message : $stdin.read(limit + 1)
95
+ raise Error, "Report input exceeds captured text bound" unless bytes && bytes.bytesize.between?(1, limit)
96
+ return bytes
97
+ end
77
98
  return File.read(message) if message && !message.empty? && File.exist?(message)
78
99
  return message if message && !message.empty?
79
100
 
@@ -7,6 +7,7 @@ module Ace
7
7
  # Internal command used by tmux-backed fork panes to launch the provider session once.
8
8
  class ForkSession < Ace::Support::Cli::Command
9
9
  include Ace::Support::Cli::Base
10
+ include AssignmentTarget
10
11
 
11
12
  desc "Run one provider-backed fork session for a subtree"
12
13
 
@@ -21,10 +22,11 @@ module Ace
21
22
 
22
23
  def initialize(launcher: nil)
23
24
  super()
24
- @launcher = launcher || Molecules::ForkSessionLauncher.new
25
+ @launcher = launcher
25
26
  end
26
27
 
27
28
  def call(**options)
29
+ refuse_protected_graph_mutation!(options)
28
30
  launcher.launch_provider_session(
29
31
  assignment_id: options[:assignment],
30
32
  fork_root: options[:root],
@@ -44,7 +46,9 @@ module Ace
44
46
 
45
47
  private
46
48
 
47
- attr_reader :launcher
49
+ def launcher
50
+ @launcher ||= Molecules::ForkSessionLauncher.new
51
+ end
48
52
  end
49
53
  end
50
54
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ require_relative "attempt/base"
3
+
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ class InboxBind < Ace::Support::Cli::Command
9
+ include Ace::Support::Cli::Base
10
+ include Attempt::Base
11
+
12
+ desc "Bind an original protected Inbox registration before scope closure"
13
+ option :mapping, desc: "Exact installed mapping ID"
14
+ option :assignment, desc: "Exact original assignment ID"
15
+ option :attempt, desc: "Exact original attempt ID"
16
+ option :event, desc: "Original Inbox event ID"
17
+ option :inbox_context, desc: "Installed Inbox context ID"
18
+ option :mutation, desc: "Stable original authority mutation ID"
19
+ option :expected_generation, desc: "Original authority mutation generation"
20
+
21
+ def call(**options)
22
+ context = protected_context(options)
23
+ raise AttemptErrors::EvidenceUnavailable, "Inbox binding requires installed protected authority" unless context
24
+ usage = "inbox-bind --mapping MAP --assignment ID --attempt ID --event ID --inbox-context ID --mutation ID --expected-generation N"
25
+ client, params, mutation = protected_attempt_request(context, options, usage)
26
+ params.merge!("event_id" => require_option(options, :event, usage),
27
+ "inbox_context_id" => require_option(options, :inbox_context, usage))
28
+ emit_json(protected_call(client, "bind_inbox", params, mutation))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require_relative "attempt/base"
3
+ require "digest"
4
+
5
+ module Ace
6
+ module Assign
7
+ module CLI
8
+ module Commands
9
+ # Local files provide bounded bytes; only the existing authority grants admission.
10
+ module ProtectedSubmission
11
+ include Attempt::Base
12
+ private
13
+
14
+ def require_option(options, key, usage)
15
+ value = options[key].to_s.strip
16
+ raise Ace::Support::Cli::Error, "Missing --#{key}: usage: ace-assign #{usage}" if value.empty?
17
+ value
18
+ end
19
+
20
+ def submission_request(options, usage)
21
+ context = protected_context(options)
22
+ raise AttemptErrors::EvidenceUnavailable, "Submission requires installed protected authority" unless context
23
+ protected_attempt_request(context, options, usage)
24
+ end
25
+
26
+ def input_bytes(path, limit:, nonempty: true)
27
+ File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
28
+ before = file.stat
29
+ raise Ace::Support::Cli::Error, "Input must be a bounded regular file" unless before.file? && before.size.between?(nonempty ? 1 : 0, limit)
30
+ bytes = file.read(limit + 1)
31
+ after = file.stat
32
+ selected = File.lstat(path)
33
+ identity = ->(stat) { [stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime] }
34
+ unless bytes.bytesize == before.size && identity.call(before) == identity.call(after) &&
35
+ identity.call(after) == identity.call(selected) && !selected.symlink?
36
+ raise Ace::Support::Cli::Error, "Input changed during bounded read"
37
+ end
38
+ bytes.freeze
39
+ end
40
+ rescue SystemCallError, IOError
41
+ raise Ace::Support::Cli::Error, "Input file is unavailable"
42
+ end
43
+
44
+ def input_json(bytes)
45
+ text = bytes.dup.force_encoding(Encoding::UTF_8)
46
+ raise Ace::Support::Cli::Error, "Input JSON is malformed" unless text.valid_encoding?
47
+ JSON.parse(text, create_additions: false, max_nesting: 32, allow_duplicate_key: false, allow_comments: false)
48
+ rescue JSON::ParserError
49
+ raise Ace::Support::Cli::Error, "Input JSON is malformed"
50
+ end
51
+
52
+ def upload_call(client, operation, params, mutation, parts, purpose)
53
+ client.call(operation, params, mutation_id: mutation, upload_parts: parts, purpose: purpose, timeout: 30).data
54
+ rescue SecurityError, Ace::Runtime::RuntimeUnavailableError, SystemCallError, IOError
55
+ protected_transport_unavailable!
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Assign
7
+ module CLI
8
+ module Commands
9
+ class Resume < Ace::Support::Cli::Command
10
+ include Ace::Support::Cli::Base
11
+ include AssignmentTarget
12
+ desc "Recover accepted assignment state without launching another writer"
13
+ option :assignment, desc: "Assignment ID"
14
+ option :mapping, desc: "Installed protected launch mapping ID"
15
+ option :attempt, desc: "Original protected attempt ID"
16
+ option :dry_run, type: :boolean, default: false, desc: "Inspect recovery without writing observations"
17
+
18
+ def call(**options)
19
+ target = resolve_assignment_target(options)
20
+ if target.prepared_input
21
+ raise Ace::Support::Cli::Error, "protected resume only supports --dry-run" unless options[:dry_run]
22
+ result = build_executor_for_target(target).status
23
+ descriptor = target.prepared_input.descriptor
24
+ puts JSON.generate(descriptor.slice("assignment_id", "mapping_id", "attempt_id", "scope", "definition_digest", "selection_sha256").merge(
25
+ "state" => result.fetch(:state).assignment_state.to_s, "dry_run" => true))
26
+ return
27
+ end
28
+ id = options[:assignment].to_s.strip
29
+ raise Ace::Support::Cli::Error, "--assignment ID is required" if id.empty?
30
+
31
+ puts JSON.generate(Organisms::AttemptCoordinator.new.resume(
32
+ assignment_id: id, dry_run: options[:dry_run]))
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -13,6 +13,7 @@ module Ace
13
13
  # ace-assign select --clear
14
14
  class Select < Ace::Support::Cli::Command
15
15
  include Ace::Support::Cli::Base
16
+ include AssignmentTarget
16
17
 
17
18
  desc "Select an assignment as the current active assignment"
18
19
 
@@ -22,6 +23,7 @@ module Ace
22
23
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
23
24
 
24
25
  def call(id: nil, **options)
26
+ refuse_protected_graph_mutation!(options)
25
27
  manager = Molecules::AssignmentManager.new
26
28
 
27
29
  if options[:clear]
@@ -12,6 +12,8 @@ module Ace
12
12
  desc "Start next workable pending step"
13
13
 
14
14
  argument :step, required: false, desc: "Step number to start (active assignment only)"
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"
@@ -41,6 +41,8 @@ module Ace
41
41
  option :format, desc: "Output format (table, json)", default: "table"
42
42
  option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
43
43
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
44
+ option :mapping, desc: "Installed protected launch mapping ID"
45
+ option :attempt, desc: "Original protected attempt ID"
44
46
  option :assignment, desc: "Show status for specific assignment ID"
45
47
  option :all, aliases: ["-a"], type: :boolean, default: false, desc: "Include completed assignments in other assignments section"
46
48
 
@@ -85,7 +87,7 @@ module Ace
85
87
  def status_to_h(assignment, state, active_steps, next_step, target:, scope_root:)
86
88
  # Extract auto_merge config if present in assignment settings or preset
87
89
  auto_merge = false
88
- preset_name = Ace::Assign::Molecules::PresetInferrer.infer_from_assignment(assignment)
90
+ preset_name = Ace::Assign::Molecules::PresetInferrer.infer_from_assignment(assignment) unless target.prepared_input
89
91
  auto_merge = true if preset_name && preset_name.start_with?("work-on-task-auto-merge")
90
92
 
91
93
  evidence = Ace::Assign::Molecules::EvidenceCalculator.calculate(
@@ -135,7 +137,7 @@ module Ace
135
137
  lines = []
136
138
  lines.concat(compact_summary_lines(view.assignment, view.scoped_state, view.active_steps, view.next_step))
137
139
 
138
- if (attempt_line = compact_attempt_line(view.assignment.id))
140
+ if !target.prepared_input && (attempt_line = compact_attempt_line(view.assignment.id))
139
141
  lines.insert(1, attempt_line)
140
142
  end
141
143
 
@@ -12,6 +12,8 @@ module Ace
12
12
  desc "Show instructions for the active, next, or explicit step"
13
13
 
14
14
  argument :step, required: false, desc: "Exact step number to inspect"
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"
@@ -24,7 +26,7 @@ module Ace
24
26
  return if options[:quiet]
25
27
 
26
28
  if inspected
27
- puts inspected.instructions
29
+ puts(target.prepared_input ? target.prepared_input.render(inspected.instructions) : inspected.instructions)
28
30
  else
29
31
  puts no_work_message(view)
30
32
  end