ace-assign 0.64.0 → 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 (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 +19 -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 +88 -9
  129. data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
@@ -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
@@ -8,11 +8,23 @@ module Ace
8
8
  module Commands
9
9
  class Resume < Ace::Support::Cli::Command
10
10
  include Ace::Support::Cli::Base
11
+ include AssignmentTarget
11
12
  desc "Recover accepted assignment state without launching another writer"
12
13
  option :assignment, desc: "Assignment ID"
14
+ option :mapping, desc: "Installed protected launch mapping ID"
15
+ option :attempt, desc: "Original protected attempt ID"
13
16
  option :dry_run, type: :boolean, default: false, desc: "Inspect recovery without writing observations"
14
17
 
15
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
16
28
  id = options[:assignment].to_s.strip
17
29
  raise Ace::Support::Cli::Error, "--assignment ID is required" if id.empty?
18
30
 
@@ -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
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ require_relative "protected_submission"
3
+ module Ace
4
+ module Assign
5
+ module CLI
6
+ module Commands
7
+ class SubmitCandidate < Ace::Support::Cli::Command
8
+ include Ace::Support::Cli::Base
9
+ include ProtectedSubmission
10
+ desc "Submit original protected worker bundle bytes"
11
+ option :mapping, desc: "Installed mapping"
12
+ option :assignment, desc: "Original assignment"
13
+ option :attempt, desc: "Original attempt"
14
+ option :head, desc: "Exact candidate head"
15
+ option :candidate_generation, desc: "Expected current candidate generation (zero before first submission)"
16
+ option :expected_generation, desc: "Persisted authority generation"
17
+ option :mutation, desc: "Stable original mutation"
18
+ option :bundle, desc: "Bounded local byte file"
19
+
20
+ def call(**options)
21
+ usage = "submit-candidate with original selectors, generation, mutation and --bundle FILE"
22
+ client, params, mutation = submission_request(options, usage)
23
+ head = require_option(options, :head, usage)
24
+ raise Ace::Support::Cli::Error, "Head must be an exact SHA-1" unless head.match?(/\A[0-9a-f]{40}\z/)
25
+ params.merge!("head" => head, "candidate_generation" => integer_option(options, :candidate_generation, usage))
26
+ bytes = input_bytes(require_option(options, :bundle, usage), limit: 64 * 1024 * 1024)
27
+ parts = [bytes]
28
+ emit_json(upload_call(client, "submit_candidate", params, mutation, parts, :candidate))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require_relative "protected_submission"
3
+ require_relative "../../authority/receipt_transfer"
4
+ module Ace
5
+ module Assign
6
+ module CLI
7
+ module Commands
8
+ class SubmitResult < Ace::Support::Cli::Command
9
+ include Ace::Support::Cli::Base
10
+ include ProtectedSubmission
11
+ desc "Submit original protected worker receipt bytes"
12
+ option :mapping, desc: "Installed mapping"
13
+ option :assignment, desc: "Original assignment"
14
+ option :attempt, desc: "Original attempt"
15
+ option :head, desc: "Exact candidate head"
16
+ option :candidate_generation, desc: "Positive candidate generation"
17
+ option :expected_generation, desc: "Persisted authority generation"
18
+ option :mutation, desc: "Stable original mutation"
19
+ option :receipt, desc: "Bounded local byte file"
20
+ option :artifact, type: :array, desc: "Ordered artifact byte files"
21
+
22
+ def call(**options)
23
+ usage = "submit-result with original selectors, generation, mutation and --receipt FILE"
24
+ client, params, mutation = submission_request(options, usage)
25
+ head = require_option(options, :head, usage)
26
+ raise Ace::Support::Cli::Error, "Head must be an exact SHA-1" unless head.match?(/\A[0-9a-f]{40}\z/)
27
+ params.merge!("head" => head, "candidate_generation" => integer_option(options, :candidate_generation, usage, positive: true))
28
+ bytes = input_bytes(require_option(options, :receipt, usage), limit: 16 * 1024)
29
+ receipt = input_json(bytes)
30
+ raise Ace::Support::Cli::Error, "Receipt must be an object" unless receipt.is_a?(Hash)
31
+ allowed = Models::ExecutionReceipt.from_h(receipt).to_h.keys
32
+ if (receipt.keys - allowed).any? || Models::ExecutionReceipt.forbidden_field?(receipt)
33
+ raise Ace::Support::Cli::Error, "Receipt contains unsupported fields"
34
+ end
35
+ unless receipt.values_at("assignment_id", "attempt_id", "head") == params.values_at("assignment_id", "attempt_id", "head") &&
36
+ Models::ExecutionReceipt::VERDICTS.include?(receipt["verdict"])
37
+ raise Ace::Support::Cli::Error, "Receipt identity or verdict differs"
38
+ end
39
+ files = options.fetch(:artifact, [])
40
+ unless files.is_a?(Array) && files.size <= 16 && !(receipt["verdict"] == "succeeded" && files.empty?)
41
+ raise Ace::Support::Cli::Error, "Receipt artifact declarations are invalid"
42
+ end
43
+ artifacts = files.map { |path| input_bytes(path, limit: 64 * 1024, nonempty: false) }
44
+ parts = [bytes, *artifacts]
45
+ digest = Digest::SHA256.hexdigest(bytes)
46
+ input = Struct.new(:parts) do
47
+ def count = parts.length
48
+ def bytes(index: 0) = parts.fetch(index)
49
+ end.new(parts)
50
+ # Reuse the maintained ordered artifact validator. This is input
51
+ # checking only; canonical result/producer admission remains Endcap's.
52
+ Ace::Assign::Authority::ReceiptTransfer.decode(input: input, receipt_sha256: digest,
53
+ artifact_field: "artifacts", reference_key: "path")
54
+ params["receipt_sha256"] = digest
55
+ emit_json(upload_call(client, "submit_result", params, mutation, parts, :receipt_artifacts))
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -55,11 +55,15 @@ require_relative "organisms/attempt_coordinator"
55
55
 
56
56
  # Commands
57
57
  require_relative "cli/commands/delivery"
58
- require_relative "cli/commands/create"
59
58
  require_relative "cli/commands/assignment_target"
59
+ require_relative "cli/commands/create"
60
60
  require_relative "cli/commands/status"
61
61
  require_relative "cli/commands/resume"
62
- require_relative "cli/commands/inbox_reconcile"
62
+ require_relative "cli/commands/inbox_bind"
63
+ require_relative "cli/commands/submit_candidate"
64
+ require_relative "cli/commands/submit_result"
65
+ require_relative "cli/commands/campaign_record_round"
66
+ require_relative "cli/commands/campaign_export_result"
63
67
  require_relative "cli/commands/step"
64
68
  require_relative "cli/commands/start"
65
69
  require_relative "cli/commands/finish"
@@ -74,6 +78,12 @@ require_relative "cli/commands/authority/serve"
74
78
  require_relative "cli/commands/authority/launch"
75
79
  require_relative "cli/commands/authority/status"
76
80
  require_relative "cli/commands/authority/terminate"
81
+ require_relative "cli/commands/authority/worker"
82
+ require_relative "cli/commands/authority/task_context"
83
+ require_relative "cli/commands/authority/task_context_principal"
84
+ require_relative "cli/commands/authority/task_context_selection"
85
+ require_relative "cli/commands/authority/inbox_context_principal"
86
+ require_relative "cli/commands/authority/inbox_context_selection"
77
87
  require_relative "cli/commands/attempt/base"
78
88
  require_relative "cli/commands/attempt/start"
79
89
  require_relative "cli/commands/attempt/status"
@@ -96,7 +106,17 @@ module Ace
96
106
  ["authority launch", "Launch one mapped native gated worker"],
97
107
  ["authority status", "Inspect protected launch ownership"],
98
108
  ["authority terminate", "Terminate the original native gated child"],
109
+ ["authority worker", "Consume original authenticated prepared work"],
110
+ ["authority task-context", "Read captured original task context through the fixed entry"],
111
+ ["authority task-context-principal", "Classify the actual caller through retained protected owners"],
112
+ ["authority task-context-selection", "Read the authenticated original context entry selection"],
113
+ ["authority inbox-context-principal", "Classify the actual caller through every retained protected owner"],
114
+ ["authority inbox-context-selection", "Read the fixed installed inbox context selection"],
99
115
  ["delivery", "Execute or reconcile attempt-bound forge delivery"],
116
+ ["submit-candidate", "Submit original protected worker candidate bytes"],
117
+ ["submit-result", "Submit original protected worker receipt and artifact bytes"],
118
+ ["campaign-export-result", "Download the original parent campaign's verified accepted result"],
119
+ ["campaign-record-round", "Consume canonical settled children into the original campaign store"],
100
120
  ["status", "Show assignment status"],
101
121
  ["step", "Show step instructions"],
102
122
  ["start", "Start next workable step"],
@@ -110,8 +130,9 @@ module Ace
110
130
  ["attempt start", "Start a scoped attempt for an assignment step"],
111
131
  ["attempt status", "Show attempt status for an assignment"],
112
132
  ["attempt evidence", "Read accepted current-head check evidence"],
113
- ["attempt finish", "Finish an attempt with a structured execution receipt"],
114
- ["attempt reconcile", "Reconcile an interrupted or uncertain attempt"]
133
+ ["attempt finish", "Finish a protected canonical result or an ordinary local receipt"],
134
+ ["attempt reconcile", "Recover an exact protected attempt or reconcile an ordinary attempt"],
135
+ ["inbox-bind", "Bind an original protected Inbox registration"],
115
136
  ].freeze
116
137
 
117
138
  HELP_EXAMPLES = [
@@ -164,6 +185,12 @@ module Ace
164
185
  register "authority launch", wrap_command(Commands::Authority::Launch)
165
186
  register "authority status", wrap_command(Commands::Authority::Status)
166
187
  register "authority terminate", wrap_command(Commands::Authority::Terminate)
188
+ register "authority worker", wrap_command(Commands::Authority::Worker)
189
+ register "authority task-context", wrap_command(Commands::Authority::TaskContext)
190
+ register "authority task-context-principal", wrap_command(Commands::Authority::TaskContextPrincipal)
191
+ register "authority task-context-selection", wrap_command(Commands::Authority::TaskContextSelection)
192
+ register "authority inbox-context-principal", wrap_command(Commands::Authority::InboxContextPrincipal)
193
+ register "authority inbox-context-selection", wrap_command(Commands::Authority::InboxContextSelection)
167
194
  register "create", wrap_command(Commands::Create)
168
195
  register "delivery", wrap_command(Commands::Delivery)
169
196
  register "status", wrap_command(Commands::Status)
@@ -183,7 +210,11 @@ register "authority terminate", wrap_command(Commands::Authority::Terminate)
183
210
  register "attempt finish", wrap_command(Commands::Attempt::Finish)
184
211
  register "attempt reconcile", wrap_command(Commands::Attempt::Reconcile)
185
212
  register "resume", wrap_command(Commands::Resume)
186
- register "inbox-reconcile", wrap_command(Commands::InboxReconcile)
213
+ register "inbox-bind", wrap_command(Commands::InboxBind)
214
+ register "submit-candidate", wrap_command(Commands::SubmitCandidate)
215
+ register "submit-result", wrap_command(Commands::SubmitResult)
216
+ register "campaign-export-result", wrap_command(Commands::CampaignExportResult)
217
+ register "campaign-record-round", wrap_command(Commands::CampaignRecordRound)
187
218
 
188
219
  # Register version command
189
220
  version_cmd = Ace::Support::Cli::VersionCommand.build(
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+ require "ace/support/cli/error"
3
+
4
+ module Ace
5
+ module Assign
6
+ # Base error class for all ace-assign exceptions.
7
+ # Inherits from Ace::Support::Cli::Error to support exception-based
8
+ # exit code pattern (per ADR-023).
9
+ #
10
+ # Subclasses should call super with appropriate exit_code.
11
+ class Error < Ace::Support::Cli::Error
12
+ def initialize(message, exit_code: 1)
13
+ super
14
+ end
15
+ end
16
+
17
+ # Assignment-related errors
18
+ module AssignmentErrors
19
+ class NotFound < Error
20
+ def initialize(message = "Assignment not found")
21
+ super(message, exit_code: 2)
22
+ end
23
+ end
24
+
25
+ class NoActive < Error
26
+ def initialize(message = "No active assignment")
27
+ super(message, exit_code: 2)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Config-related errors
33
+ module ConfigErrors
34
+ class NotFound < Error
35
+ def initialize(message = "Configuration not found")
36
+ super(message, exit_code: 3)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Step-related errors
42
+ module StepErrors
43
+ class NotFound < Error
44
+ def initialize(message = "Step not found")
45
+ super(message, exit_code: 4)
46
+ end
47
+ end
48
+
49
+ class InvalidState < Error; end
50
+ end
51
+
52
+ # Attempt/evidence-related errors (exit code 5)
53
+ module AttemptErrors
54
+ class InvalidTransition < Error
55
+ def initialize(message = "Illegal attempt state transition")
56
+ super(message, exit_code: 5)
57
+ end
58
+ end
59
+
60
+ class InvalidScope < Error
61
+ def initialize(message = "Invalid attempt scope")
62
+ super(message, exit_code: 5)
63
+ end
64
+ end
65
+
66
+ # Duplicate start with a conflicting binding for an owned subtree
67
+ class Conflict < Error
68
+ def initialize(message = "Conflicting active attempt for scope")
69
+ super(message, exit_code: 5)
70
+ end
71
+ end
72
+
73
+ class NotFound < Error
74
+ def initialize(message = "Attempt not found")
75
+ super(message, exit_code: 5)
76
+ end
77
+ end
78
+
79
+ # Untrusted or unknown execution identity; authority fails closed
80
+ class UnauthorizedIdentity < Error
81
+ def initialize(message = "Unknown or unauthorized execution identity")
82
+ super(message, exit_code: 5)
83
+ end
84
+ end
85
+
86
+ # Receipt failed verification (binding, head, digest, artifacts, review)
87
+ class ReceiptRejected < Error
88
+ def initialize(message = "Receipt rejected")
89
+ super(message, exit_code: 5)
90
+ end
91
+ end
92
+
93
+ # Byte framing failed before any business receipt could be admitted.
94
+ class MalformedTransfer < ReceiptRejected; end
95
+
96
+ # Recovery settled a prior effect, but did not perform this request.
97
+ class CurrentEffectRequired < ReceiptRejected
98
+ def initialize(operation:, recovered_operation:)
99
+ super("Recovered #{recovered_operation}; requested #{operation} still needs execution")
100
+ end
101
+ end
102
+
103
+ # Attempt state forbids the requested effect (terminal, uncertain, blocked)
104
+ class InvalidState < Error
105
+ def initialize(message = "Attempt state forbids the requested operation")
106
+ super(message, exit_code: 5)
107
+ end
108
+ end
109
+
110
+ # Evidence storage is missing or unwritable; external effects are blocked
111
+ class EvidenceUnavailable < Error
112
+ def initialize(message = "Evidence storage unavailable")
113
+ super(message, exit_code: 5)
114
+ end
115
+ end
116
+
117
+ # Complete maintenance exclusions are busy or their admission deadline expired.
118
+ class MaintenanceBusy < EvidenceUnavailable
119
+ end
120
+
121
+ # One canonical inventory row cannot fit the fixed protocol frame.
122
+ class BoundedResultUnavailable < EvidenceUnavailable
123
+ end
124
+
125
+ # Exhaustively authenticated service inventory remains unsettled.
126
+ class ServiceSettlementPending < EvidenceUnavailable
127
+ end
128
+
129
+ # Exhaustively authenticated current Inbox claims remain queued.
130
+ class InboxSettlementPending < EvidenceUnavailable
131
+ end
132
+
133
+ # Positively admitted original Inbox work remains pending, not corrupt.
134
+ class InboxContextPending < EvidenceUnavailable
135
+ end
136
+ end
137
+
138
+ end
139
+ end
@@ -20,7 +20,7 @@ module Ace
20
20
  # but tests would need to be updated to provide all required fields.
21
21
  class Assignment
22
22
  attr_reader :id, :name, :description, :created_at, :updated_at, :source_config, :cache_dir, :parent,
23
- :task_id, :project_id
23
+ :task_id, :project_id, :campaign_execution, :review_campaign
24
24
 
25
25
  # @param id [String] Unique assignment ID (6-char compact timestamp)
26
26
  # @param name [String] Human-readable assignment name
@@ -33,7 +33,7 @@ module Ace
33
33
  # @param task_id [String, nil] Source task ID attached to this assignment (nil = taskless)
34
34
  # @param project_id [String, nil] Project ID this assignment executes in
35
35
  def initialize(id:, name:, created_at:, source_config:, description: nil, updated_at: nil, cache_dir: nil,
36
- parent: nil, task_id: nil, project_id: nil)
36
+ parent: nil, task_id: nil, project_id: nil, campaign_execution: nil, review_campaign: nil)
37
37
  @id = id.freeze
38
38
  @name = name.freeze
39
39
  @description = description&.freeze
@@ -44,6 +44,24 @@ module Ace
44
44
  @parent = parent&.freeze
45
45
  @task_id = task_id&.freeze
46
46
  @project_id = project_id&.freeze
47
+ if !campaign_execution.nil? && !review_campaign.nil?
48
+ raise ArgumentError, "assignment cannot own both parent campaign and child execution"
49
+ end
50
+ @campaign_execution = immutable_campaign_execution(campaign_execution) unless campaign_execution.nil?
51
+ @review_campaign = immutable_campaign_execution(review_campaign) unless review_campaign.nil?
52
+ end
53
+
54
+ # Keep registered definition bytes stable without retaining caller-owned
55
+ # mutable objects. Authority validates the closed linkage and provenance.
56
+ private def immutable_campaign_execution(value)
57
+ case value
58
+ when Hash
59
+ value.to_h { |key, item| [immutable_campaign_execution(key), immutable_campaign_execution(item)] }.freeze
60
+ when Array then value.map { |item| immutable_campaign_execution(item) }.freeze
61
+ when String then value.dup.freeze
62
+ when Integer, NilClass, TrueClass, FalseClass then value
63
+ else raise ArgumentError, "invalid campaign execution value"
64
+ end
47
65
  end
48
66
 
49
67
  # @return [Boolean] True when a source task is attached; taskless
@@ -65,7 +83,9 @@ module Ace
65
83
  "source_config" => source_config,
66
84
  "parent" => parent,
67
85
  "task_id" => task_id,
68
- "project_id" => project_id
86
+ "project_id" => project_id,
87
+ "campaign_execution" => campaign_execution,
88
+ "review_campaign" => review_campaign
69
89
  }.compact
70
90
  end
71
91
 
@@ -84,7 +104,9 @@ module Ace
84
104
  cache_dir: cache_dir,
85
105
  parent: data["parent"],
86
106
  task_id: data["task_id"],
87
- project_id: data["project_id"]
107
+ project_id: data["project_id"],
108
+ campaign_execution: data["campaign_execution"],
109
+ review_campaign: data["review_campaign"]
88
110
  )
89
111
  end
90
112