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,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module Authority
6
+ class Endcap
7
+ BIND_INBOX_PARAMETERS = %w[mapping_id assignment_id attempt_id expected_generation event_id inbox_context_id].freeze
8
+ FINISH_PARAMETERS = %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head result_id].freeze
9
+ RECOVERY_PARAMETERS = %w[mapping_id assignment_id attempt_id expected_generation].freeze
10
+
11
+ # Called only by the canonical terminal owner at its authenticated
12
+ # introduction prefix, using the original deployment mapping.
13
+ def finished_review_evidence!(journal:, events:, params:, map:, commit:)
14
+ current = exact_candidate!(retained_candidate(events, params.fetch("candidate_generation")), params)
15
+ accepted = approved_review!(journal, events, params, map, current, commit: commit)
16
+ result = verified_result(journal, events, params, map, retained_origin(events, params), current, commit)
17
+ raise AttemptErrors::EvidenceUnavailable, "finished canonical result is missing" unless result
18
+ campaign_finished_result!(journal: journal, commit: commit, events: events, params: params, map: map,
19
+ result: result, accepted: accepted)
20
+ @launch.campaign_children_settled!(journal: journal, commit: commit, params: params, map: map)
21
+ accepted
22
+ end
23
+
24
+ private
25
+
26
+ def dispatch_recover(request:, peer:, role:, transfer: nil)
27
+ raise ArgumentError, "Recovery forbids transfer" unless transfer.nil?
28
+ params, map = attempt_consumer_request!(request, RECOVERY_PARAMETERS)
29
+ selected_commit = nil
30
+ journal = @launch.journals.fetch(map.fetch("project_id"))
31
+ readonly = @launch.with_assignment(params: params, map: map) do |selected, _|
32
+ protected_journal!(selected)
33
+ selected_commit = selected.ref_value
34
+ events = @launch.preview_attempt_events!(journal: selected, commit: selected_commit, params: params, map: map)
35
+ finish_identity!(events, params, map, peer, role)
36
+ accepted = selected.mutation_result(request.fetch("mutation_id"), commit: selected_commit)
37
+ if accepted
38
+ verify_recovery_reply!(selected, accepted, request, params, map)
39
+ next({data: accepted.fetch("data").merge("journal_commit" => accepted.fetch("journal_commit")), replayed: true})
40
+ end
41
+ state = Molecules::CanonicalAttemptState.derive(events)
42
+ next unless %w[succeeded failed stopped].include?(state)
43
+ terminal = @launch.completion_terminal!(journal: selected, commit: selected_commit, params: params, map: map)
44
+ {data: {"attempt_id" => params.fetch("attempt_id"), "state" => terminal.fetch("canonical_state"),
45
+ "decision" => "restart-required", "reason" => "terminal_attempt", "generation" => terminal.fetch("generation"),
46
+ "journal_commit" => selected_commit}, replayed: false}
47
+ end
48
+ return readonly if readonly
49
+ recover_under_exclusion!(request, params, map, peer, role, selected_commit)
50
+ end
51
+
52
+ def recover_under_exclusion!(request, params, map, peer, role, selected_commit)
53
+ @launch.with_assignment(params: params, map: map, exclusive: true) do |journal, _|
54
+ unless journal.ref_value == selected_commit
55
+ raise AttemptErrors::Conflict, "Recovery canonical selection advanced"
56
+ end
57
+ events = @launch.preview_attempt_events!(journal: journal, commit: selected_commit, params: params, map: map)
58
+ finish_identity!(events, params, map, peer, role)
59
+ journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
60
+ mutation_id: request.fetch("mutation_id"), operation: "recover", parameters_digest: Atoms::EvidenceDigest.digest(params),
61
+ expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, prefix, _generation|
62
+ unless prefix == selected_commit
63
+ raise AttemptErrors::Conflict, "Recovery canonical selection advanced"
64
+ end
65
+ origin = finish_identity!(fresh, params, map, peer, role)
66
+ state = Molecules::CanonicalAttemptState.derive(fresh)
67
+ effects_pending = recovery_pending_settlement do
68
+ service_settlement_evidence!(journal: journal, events: fresh, params: params, map: map, commit: prefix)
69
+ end
70
+ bound = %w[bound issued].include?(origin["phase"])
71
+ observation = if !effects_pending && state == "running" && bound
72
+ @launch.recovery_observation!(journal: journal, events: fresh, params: params, map: map, commit: prefix)
73
+ else
74
+ {"liveness" => "unknown"}
75
+ end
76
+ decision, reason = Organisms::AttemptCoordinator.protected_recovery_decision(state: state,
77
+ effects_pending: effects_pending, bound: bound,
78
+ live: observation["liveness"] == "live", checkpoint: true)
79
+ plan = Organisms::AttemptCoordinator.protected_recovery_plan(events: fresh, decision: decision, reason: reason, observation: observation)
80
+ plan.except(:state).merge(data: {"attempt_id" => params.fetch("attempt_id"), "state" => plan.fetch(:state),
81
+ "decision" => decision, "reason" => reason})
82
+ end
83
+ end
84
+ end
85
+
86
+ def recovery_pending_settlement
87
+ yield
88
+ false
89
+ rescue AttemptErrors::ServiceSettlementPending
90
+ true
91
+ end
92
+
93
+ def verify_recovery_reply!(journal, accepted, request, params, map)
94
+ unless accepted["operation"] == "recover" && accepted["parameters_digest"] == Atoms::EvidenceDigest.digest(params) &&
95
+ accepted.values_at("assignment_id", "attempt_id") == params.values_at("assignment_id", "attempt_id")
96
+ raise AttemptErrors::Conflict, "Recovery mutation is already bound to different input"
97
+ end
98
+ prefix = accepted.fetch("journal_commit")
99
+ events = @launch.preview_attempt_events!(journal: journal, commit: prefix, params: params, map: map)
100
+ index = events.index { |event| event.dig("payload", "mutation_id") == request.fetch("mutation_id") && event.dig("payload", "operation") == "recover" }
101
+ raise AttemptErrors::EvidenceUnavailable, "Recovery acceptance is missing" unless index && index.positive?
102
+ before = events.take(index)
103
+ transition = before.last["type"] == "transition" && before.last.dig("payload", "to") == "uncertain" ? before.pop : nil
104
+ observation = before.last
105
+ data = accepted.fetch("data")
106
+ reasons = %w[live_owner unresolved_effect unresolved_inbox launch_unbound scope_unverifiable attempt_uncertain checkpoint_unavailable]
107
+ unless data.is_a?(Hash) && data.keys.sort == %w[attempt_id state decision reason generation].sort &&
108
+ data["attempt_id"] == params.fetch("attempt_id") && data["generation"].is_a?(Integer) &&
109
+ data["generation"] == journal.authority_generation(events) && reasons.include?(data["reason"]) &&
110
+ ((data["decision"] == "adopt" && data["reason"] == "live_owner" && data["state"] == "running") ||
111
+ (data["decision"] == "reconcile-required" && data["reason"] != "live_owner")) &&
112
+ events[index]["type"] == "authority_mutation" && index == events.length - 1
113
+ raise AttemptErrors::EvidenceUnavailable, "Recovery reply schema or accepted prefix differs"
114
+ end
115
+ unless observation && observation["type"] == "recovery_observation" && observation.dig("payload", "decision") == data["decision"] &&
116
+ observation.dig("payload", "recovery_reason") == data["reason"] && observation.dig("payload", "observation").is_a?(Hash) &&
117
+ (!transition || transition.fetch("payload") == {"from" => "running", "to" => "uncertain", "reason" => data.fetch("reason")}) &&
118
+ Molecules::CanonicalAttemptState.derive(events) == data.fetch("state")
119
+ raise AttemptErrors::EvidenceUnavailable, "Recovery observation acceptance differs"
120
+ end
121
+ binding = retained_origin(events, params).fetch("process_binding", nil)
122
+ observed = observation.dig("payload", "observation")
123
+ if observed["liveness"] == "live" && (!binding || observed["runtime_binding"] != binding || observed["identity"] != binding.fetch("process_identity"))
124
+ raise AttemptErrors::EvidenceUnavailable, "Recovery original live binding differs"
125
+ end
126
+ true
127
+ end
128
+
129
+ def dispatch_finish(request:, peer:, role:, transfer: nil)
130
+ raise ArgumentError, "Result finish forbids transfer" unless transfer.nil?
131
+ params, map = attempt_consumer_request!(request, FINISH_PARAMETERS)
132
+ unless params["candidate_generation"].is_a?(Integer) && params["candidate_generation"].positive? &&
133
+ params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
134
+ params["result_id"].is_a?(String) && params["result_id"].match?(/\A[0-9a-f]{32}\z/)
135
+ raise ArgumentError, "Result finish selectors differ"
136
+ end
137
+ journal = @launch.journals.fetch(map.fetch("project_id"))
138
+ replay = @launch.with_assignment(params: params, map: map) do |selected, _|
139
+ commit = selected.ref_value
140
+ accepted = selected.mutation_result(request.fetch("mutation_id"), commit: commit)
141
+ next unless accepted
142
+ unless accepted["operation"] == "finish" && accepted["parameters_digest"] == Atoms::EvidenceDigest.digest(params) &&
143
+ accepted.values_at("assignment_id", "attempt_id") == params.values_at("assignment_id", "attempt_id")
144
+ raise AttemptErrors::Conflict, "Finish mutation is already bound to different input"
145
+ end
146
+ events = @launch.preview_attempt_events!(journal: selected, commit: commit, params: params, map: map)
147
+ finish_identity!(events, params, map, peer, role)
148
+ @launch.completion_terminal!(journal: selected, commit: commit, params: params, map: map)
149
+ {data: accepted.fetch("data").merge("journal_commit" => accepted.fetch("journal_commit")), replayed: true}
150
+ end
151
+ return replay if replay
152
+ @launch.with_assignment(params: params, map: map, exclusive: true) do |selected, _|
153
+ protected_journal!(selected)
154
+ commit = selected.ref_value
155
+ events = @launch.preview_attempt_events!(journal: selected, commit: commit, params: params, map: map)
156
+ finish_identity!(events, params, map, peer, role)
157
+ replay = selected.mutation_result(request.fetch("mutation_id"), commit: commit)
158
+ if replay && replay["operation"] == "finish"
159
+ @launch.completion_terminal!(journal: selected, commit: commit, params: params, map: map)
160
+ end
161
+ with_campaign_finish!(journal: selected, commit: commit, events: events, params: params, map: map) do
162
+ selected.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
163
+ mutation_id: request.fetch("mutation_id"), operation: "finish", parameters_digest: Atoms::EvidenceDigest.digest(params),
164
+ expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, prefix, _generation|
165
+ origin = finish_identity!(fresh, params, map, peer, role)
166
+ unless Molecules::CanonicalAttemptState.derive(fresh) == "running" && %w[bound issued].include?(origin["phase"])
167
+ raise AttemptErrors::Conflict, "Result finish requires an active bound attempt"
168
+ end
169
+ current = exact_candidate!(candidate(fresh), params)
170
+ result = verified_result(selected, fresh, params, map, origin, current, prefix, result_id: params.fetch("result_id"))
171
+ unless result && result.fetch("result_id") == params.fetch("result_id")
172
+ raise AttemptErrors::EvidenceUnavailable, "Result finish requires its exact submitted receipt"
173
+ end
174
+ if result.dig("receipt", "verdict") == "succeeded"
175
+ accepted = approved_review!(selected, fresh, params, map, current, commit: prefix)
176
+ campaign_finished_result!(journal: selected, commit: prefix, events: fresh, params: params, map: map,
177
+ result: result, accepted: accepted)
178
+ end
179
+ @launch.campaign_children_settled!(journal: selected, commit: prefix, params: params, map: map)
180
+ @launch.completion_scope!(journal: selected, events: fresh, params: params, map: map, commit: prefix)
181
+ service_settlement_evidence!(journal: selected, events: fresh, params: params, map: map, commit: prefix)
182
+ plan = Organisms::AttemptCoordinator.finished_transition_plan(events: fresh, receipt: result.fetch("receipt"))
183
+ plan.merge(data: params.slice("attempt_id", "result_id", "head", "candidate_generation").merge(
184
+ "receipt_digest" => result.fetch("receipt_digest"), "state" => result.dig("receipt", "verdict")))
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ def finish_identity!(events, params, map, peer, role)
191
+ unless %i[launcher supervisor].include?(role)
192
+ raise AttemptErrors::UnauthorizedIdentity, "Result finish requires mapped completion owner"
193
+ end
194
+ origin = retained_origin(events, params)
195
+ purpose_peer!(events, params, map, origin, peer, role, kind: "result")
196
+ origin
197
+ end
198
+
199
+ def attempt_consumer_request!(request, fields)
200
+ params = request.fetch("params")
201
+ unless params.is_a?(Hash) && params.keys.sort == fields.sort
202
+ raise ArgumentError, "attempt consumer parameters differ"
203
+ end
204
+ %w[mapping_id assignment_id attempt_id].each { |key| result_id!(params.fetch(key)) }
205
+ result_id!(request.fetch("mutation_id"))
206
+ unless params["expected_generation"].is_a?(Integer) && params["expected_generation"] >= 0
207
+ raise ArgumentError, "attempt consumer generation differs"
208
+ end
209
+ map = @deployment.mapping(params.fetch("mapping_id"))
210
+ unless map.fetch("project_id") == request.fetch("project_id")
211
+ raise AttemptErrors::UnauthorizedIdentity, "attempt consumer project differs"
212
+ end
213
+ [params, map]
214
+ end
215
+
216
+ # Original registration comes from the admitted fixed Inbox owner, not
217
+ # from peer fields or a current filesystem receipt. Bind and seal share
218
+ # the same canonical slot/task/assignment exclusion and CAS.
219
+ def dispatch_bind_inbox(request:, peer:, role:, transfer: nil)
220
+ raise ArgumentError, "Inbox binding forbids transfer" unless transfer.nil?
221
+ params, map = attempt_consumer_request!(request, BIND_INBOX_PARAMETERS)
222
+ %w[event_id inbox_context_id].each { |key| result_id!(params.fetch(key)) }
223
+ with_inbox_context(params, map, mutation_id: request.fetch("mutation_id")) do |session|
224
+ @launch.with_assignment(params: params, map: map, exclusive: true) do |journal, _|
225
+ protected_journal!(journal)
226
+ commit = journal.ref_value
227
+ events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: map)
228
+ binding = bind_inbox_selection!(events, params, map, peer, role, session)
229
+ replay = journal.mutation_result(request.fetch("mutation_id"), commit: commit)
230
+ verify_inbox_binding_reply!(events, replay, binding) if replay && replay["operation"] == "bind_inbox"
231
+ journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
232
+ mutation_id: request.fetch("mutation_id"), operation: "bind_inbox", parameters_digest: Atoms::EvidenceDigest.digest(params),
233
+ expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, _prefix, _generation|
234
+ @launch.scope_open_for_effect!(events: fresh, params: params, map: map)
235
+ selected = bind_inbox_selection!(fresh, params, map, peer, role, session)
236
+ existing = fresh.select { |event| event["type"] == "inbox_binding" && event.dig("payload", "event_id") == params.fetch("event_id") }
237
+ unless existing.empty? || (existing.one? && existing.first.fetch("payload") == selected)
238
+ raise AttemptErrors::Conflict, "Inbox registration is already bound differently"
239
+ end
240
+ {events: existing.empty? ? [{type: "inbox_binding", payload: selected}] : [], blobs: {}, data: selected}
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ def bind_inbox_selection!(events, params, map, peer, role, session)
247
+ unless Molecules::CanonicalAttemptState.derive(events) == "running"
248
+ raise AttemptErrors::EvidenceUnavailable, "Inbox binding requires an active original worker"
249
+ end
250
+ submitting_worker!(events, params, map, peer, role)
251
+ context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
252
+ lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
253
+ assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: context.fetch("native_mapping_id"))
254
+ original = params.slice("assignment_id", "mapping_id", "inbox_context_id").merge("project_id" => map.fetch("project_id"))
255
+ record = session.request("status_context", {"event_id" => params.fetch("event_id"),
256
+ "attempt_id" => params.fetch("attempt_id"), "original" => original}).fetch("record")
257
+ registration = record.slice(*INBOX_REGISTRATION_FIELDS)
258
+ unless inbox_registration?(registration) && registration.values_at("event_id", "attempt_id") == params.values_at("event_id", "attempt_id")
259
+ raise AttemptErrors::EvidenceUnavailable, "Inbox original registration differs"
260
+ end
261
+ inbox_native_lineage!(record, lineage)
262
+ params.slice("event_id", "attempt_id", "inbox_context_id").merge("registration" => registration)
263
+ end
264
+
265
+ def verify_inbox_binding_reply!(events, reply, selected)
266
+ bindings = events.select { |event| event["type"] == "inbox_binding" && event.fetch("payload") == selected }
267
+ unless bindings.one? && reply.fetch("data").except("generation") == selected
268
+ raise AttemptErrors::EvidenceUnavailable, "Inbox binding replay provenance differs"
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module Authority
6
+ class Endcap
7
+ # Internal R1 dependency. The fixed authority consumer supplies the
8
+ # original expected linkage; no metadata document can attest execution.
9
+ # Raw imported artifacts are read at this same immutable prefix.
10
+ def with_campaign_execution_evidence!(journal:, commit:, params:, map:, receipt_digest:, execution_binding:)
11
+ raise ArgumentError, "campaign evidence requires a block" unless block_given?
12
+ begin
13
+ row = @launch.completion_terminal!(journal: journal, commit: commit, params: params, map: map)
14
+ unless row["canonical_state"] == "succeeded" && row["reservation_release_event_id"]
15
+ raise AttemptErrors::EvidenceUnavailable, "campaign execution is not succeeded and released"
16
+ end
17
+ definition = @launch.preview_attempt_definition!(journal: journal, commit: commit, params: params, map: map)
18
+ execution = definition.campaign_execution
19
+ raise AttemptErrors::EvidenceUnavailable, "campaign execution linkage is missing" unless execution
20
+ binding = execution.merge(params.slice("assignment_id", "attempt_id"),
21
+ "definition_digest" => row.fetch("definition_digest"), "binding_digest" => row.fetch("original_binding_digest"))
22
+ unless binding == execution_binding && binding["binding_digest"].is_a?(String)
23
+ raise AttemptErrors::EvidenceUnavailable, "campaign execution original linkage differs"
24
+ end
25
+ inventory = journal.canonical_event_inventory!(commit: commit)
26
+ accepted_commit = inventory.fetch("introductions").fetch(params.fetch("assignment_id"))
27
+ .fetch(row.fetch("terminal_event_id"))
28
+ events = @launch.preview_attempt_events!(journal: journal, commit: accepted_commit, params: params, map: map)
29
+ results = events.select { |event| event["type"] == "result_submitted" &&
30
+ event.dig("payload", "receipt_digest") == receipt_digest }
31
+ raise AttemptErrors::EvidenceUnavailable, "campaign execution result is ambiguous" unless results.one?
32
+ submitted = results.first.fetch("payload")
33
+ selected = params.merge(submitted.fetch("binding").slice("head", "candidate_generation"),
34
+ "result_id" => submitted.fetch("result_id"))
35
+ current = exact_candidate!(retained_candidate(events, selected.fetch("candidate_generation")), selected)
36
+ result = verified_result(journal, events, selected, map, retained_origin(events, selected), current, accepted_commit,
37
+ result_id: selected.fetch("result_id"))
38
+ unless result && result["receipt_digest"] == receipt_digest && result.dig("receipt", "verdict") == "succeeded"
39
+ raise AttemptErrors::EvidenceUnavailable, "campaign execution accepted receipt differs"
40
+ end
41
+ terminal = events.find { |event| event["digest"] == row.fetch("terminal_event_id") }
42
+ unless terminal && terminal["type"] == "receipt_accepted" && terminal.dig("payload", "receipt") == result.fetch("receipt")
43
+ raise AttemptErrors::EvidenceUnavailable, "campaign execution terminal receipt differs"
44
+ end
45
+ accepted = approved_review!(journal, events, selected, map, current, commit: accepted_commit)
46
+ campaign_finished_result!(journal: journal, commit: accepted_commit, events: events, params: selected, map: map,
47
+ result: result, accepted: accepted)
48
+ proof = result.fetch("receipt").merge("attempt_id" => params.fetch("attempt_id"),
49
+ "receipt_digest" => receipt_digest, "execution_binding" => binding, "journal_commit" => accepted_commit)
50
+ freeze_campaign_evidence!(proof)
51
+ rescue KeyError, TypeError, ArgumentError, AttemptErrors::ReceiptRejected
52
+ raise AttemptErrors::EvidenceUnavailable, "campaign execution evidence is unavailable"
53
+ end
54
+ active = true
55
+ thread = Thread.current
56
+ canonical = Molecules::CanonicalEvidence.new(journal: journal)
57
+ reader = ->(artifact) {
58
+ unless active && Thread.current.equal?(thread) && result.fetch("artifacts").include?(artifact)
59
+ raise AttemptErrors::EvidenceUnavailable, "campaign artifact read escaped its original evidence scope"
60
+ end
61
+ canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
62
+ **result_context(result.fetch("binding")), commit: accepted_commit)
63
+ }
64
+ yield proof, reader
65
+ ensure
66
+ active = false
67
+ end
68
+
69
+ private
70
+
71
+ def campaign_execution_dependencies!(journal:, commit:, parent_params:, parent_map:)
72
+ # This closure belongs to one held original exclusion and immutable
73
+ # journal selection. Never retained across RPCs. Live/original source
74
+ # parent checks below still run on every callback; only fully verified raw
75
+ # canonical artifacts and their introduced-prefix proof are reused.
76
+ verified = {}
77
+ selected_rows = {}
78
+ inventory = nil
79
+ read = lambda do |reference, phase:, head:, artifacts: nil, name: nil, producer: nil, reviewer: nil, historical: false|
80
+ unless reference.is_a?(Hash) && reference.keys.sort == %w[attempt_id digest] &&
81
+ reference["digest"].is_a?(String) && reference["digest"].match?(DIGEST)
82
+ raise AttemptErrors::EvidenceUnavailable, "campaign child receipt reference differs"
83
+ end
84
+ selection_key = Atoms::EvidenceDigest.digest("commit" => commit, "reference" => reference,
85
+ "phase" => phase, "head" => head, "name" => name)
86
+ params, map, execution, row = selected_rows[selection_key] ||= begin
87
+ inventory ||= journal.canonical_event_inventory!(commit: commit)
88
+ matches = inventory.fetch("events").filter_map do |assignment, events|
89
+ next unless events.any? { |event| event["attempt_id"] == reference["attempt_id"] }
90
+ registration = events.reverse.find { |event| event.dig("payload", "operation") == "register_assignment" }&.dig("payload", "data")
91
+ next unless registration
92
+ descriptor = @launch.send(:control_original_descriptor!, registration.fetch("lifecycle_control"))
93
+ child_map = descriptor.mapping(registration.fetch("mapping_id"))
94
+ params = {"assignment_id" => assignment, "attempt_id" => reference.fetch("attempt_id"),
95
+ "mapping_id" => registration.fetch("mapping_id")}
96
+ definition = @launch.preview_attempt_definition!(journal: journal, commit: commit, params: params, map: child_map)
97
+ execution = definition.campaign_execution
98
+ next unless execution && execution.values_at("parent_assignment_id", "parent_attempt_id", "phase", "head") ==
99
+ parent_params.values_at("assignment_id", "attempt_id") + [phase, head]
100
+ next unless child_map["project_id"] == parent_map.fetch("project_id")
101
+ next if name && execution["check_name"] != name
102
+ [params, child_map, execution]
103
+ end
104
+ raise AttemptErrors::EvidenceUnavailable, "campaign child receipt linkage is ambiguous" unless matches.one?
105
+ params, map, execution = matches.first
106
+ row = @launch.completion_terminal!(journal: journal, commit: commit, params: params, map: map)
107
+ [params.freeze, map, freeze_campaign_evidence!(JSON.parse(JSON.generate(execution))),
108
+ freeze_campaign_evidence!(JSON.parse(JSON.generate(row)))]
109
+ end
110
+ binding = execution.merge(params.slice("assignment_id", "attempt_id"),
111
+ "definition_digest" => row.fetch("definition_digest"), "binding_digest" => row.fetch("original_binding_digest"))
112
+ unless historical
113
+ campaign_parent_candidate!(journal: journal, commit: commit, params: parent_params.merge(
114
+ "scope" => execution.fetch("parent_scope"), "head" => head, "base" => execution.fetch("base"),
115
+ "candidate_generation" => execution.fetch("parent_candidate_generation")), map: parent_map)
116
+ end
117
+ key = Atoms::EvidenceDigest.digest("journal" => journal.object_id, "commit" => commit,
118
+ "binding" => binding, "receipt" => reference, "phase" => phase, "head" => head,
119
+ "name" => name, "producer" => producer, "reviewer" => reviewer, "artifacts" => artifacts)
120
+ proof = verified[key] ||= with_campaign_execution_evidence!(journal: journal, commit: commit, params: params, map: map,
121
+ receipt_digest: reference.fetch("digest"), execution_binding: binding) do |canonical, reader|
122
+ canonical.fetch("artifacts").each do |artifact|
123
+ unless Digest::SHA256.hexdigest(reader.call(artifact)) == artifact.fetch("sha256")
124
+ raise AttemptErrors::EvidenceUnavailable, "canonical campaign artifact bytes differ"
125
+ end
126
+ end
127
+ freeze_campaign_evidence!(JSON.parse(JSON.generate(canonical)))
128
+ end
129
+ if producer && (proof.dig("producer", "actor") != producer || proof.dig("review", "reviewer", "actor") != reviewer)
130
+ raise AttemptErrors::EvidenceUnavailable, "campaign approval original actors differ"
131
+ end
132
+ if artifacts && artifacts.any? { |artifact| !proof.fetch("artifacts").any? { |ref| ref["sha256"] == artifact.fetch("sha256") } }
133
+ raise AttemptErrors::EvidenceUnavailable, "campaign artifact differs from canonical child bytes"
134
+ end
135
+ proof.merge("artifacts" => artifacts || proof.fetch("artifacts"), "historical" => historical)
136
+ end
137
+ {check_evidence: ->(reference, head:, name:, historical: false) {
138
+ read.call(reference, phase: "check", head: head, name: name, historical: historical)
139
+ }, review_evidence: ->(reference, head:, artifacts:, historical: false) {
140
+ read.call(reference, phase: "collection", head: head, artifacts: artifacts, historical: historical)
141
+ }, approval_evidence: ->(reference, head:, artifacts:, producer:, reviewer:, historical: false) {
142
+ read.call(reference, phase: "approval", head: head, artifacts: artifacts, producer: producer, reviewer: reviewer, historical: historical)
143
+ }}.freeze
144
+ end
145
+
146
+ def freeze_campaign_evidence!(value)
147
+ case value
148
+ when Hash then value.each { |key, item| freeze_campaign_evidence!(key); freeze_campaign_evidence!(item) }
149
+ when Array then value.each { |item| freeze_campaign_evidence!(item) }
150
+ end
151
+ value.freeze
152
+ end
153
+
154
+ # The ordinary result is still campaign-free. Its phase comes only
155
+ # from the authenticated original registered child definition.
156
+ def campaign_child_result!(journal:, commit:, events:, params:, map:, receipt:)
157
+ definition = @launch.preview_attempt_definition!(journal: journal, commit: commit,
158
+ params: params, map: map)
159
+ execution = definition.campaign_execution
160
+ return unless execution
161
+ CampaignExecution.validate!(execution, parent: definition.parent,
162
+ policy: campaign_original_parent_policy!(journal, commit, execution, map))
163
+ intent = events.find { |event| event["type"] == "intent" }&.fetch("payload")
164
+ CampaignExecution.validate_result!(execution, receipt: receipt, base: intent&.fetch("base_head"))
165
+ execution
166
+ rescue KeyError, TypeError, ArgumentError
167
+ raise AttemptErrors::ReceiptRejected, "campaign child result binding is incomplete"
168
+ end
169
+
170
+ def campaign_finished_result!(journal:, commit:, events:, params:, map:, result:, accepted:)
171
+ execution = campaign_child_result!(journal: journal, commit: commit, events: events, params: params, map: map,
172
+ receipt: result.fetch("receipt"))
173
+ return unless execution
174
+ CampaignExecution.validate_finished_review!(execution, receipt: result.fetch("receipt"),
175
+ accepted_review: accepted.fetch("review_receipt"))
176
+ rescue KeyError, ArgumentError, TypeError
177
+ raise AttemptErrors::ReceiptRejected, "campaign independent review differs from executed result"
178
+ end
179
+
180
+ def campaign_original_parent_policy!(journal, commit, execution, map)
181
+ campaign_original_parent_context!(journal, commit, execution, map).fetch(:selected).fetch("policy")
182
+ end
183
+
184
+ def campaign_original_parent_context!(journal, commit, execution, map)
185
+ parent = {"assignment_id" => execution.fetch("parent_assignment_id"),
186
+ "attempt_id" => execution.fetch("parent_attempt_id")}
187
+ inventory = journal.canonical_event_inventory!(commit: commit)
188
+ events = inventory.fetch("events").fetch(parent.fetch("assignment_id"))
189
+ reservations = events.select { |event| event["attempt_id"] == parent.fetch("attempt_id") &&
190
+ event.dig("payload", "operation") == "reserve_attempt" }
191
+ raise AttemptErrors::EvidenceUnavailable, "campaign parent original reservation differs" unless reservations.one?
192
+ parent["mapping_id"] = reservations.first.dig("payload", "data", "mapping_id")
193
+ prefix = inventory.fetch("introductions").fetch(parent.fetch("assignment_id"))
194
+ .fetch(reservations.first.fetch("digest"))
195
+ registration = @launch.send(:definition, journal, parent.fetch("assignment_id"), commit: prefix)
196
+ descriptor = @launch.send(:control_original_descriptor!, registration.fetch("lifecycle_control"))
197
+ original = @launch.send(:control_registration_context!, parent,
198
+ descriptor.mapping(parent.fetch("mapping_id")), journal, commit: commit)
199
+ unless original.fetch(:map).fetch("project_id") == map.fetch("project_id")
200
+ raise AttemptErrors::EvidenceUnavailable, "campaign child original project differs"
201
+ end
202
+ definition = @launch.preview_attempt_definition!(journal: journal, commit: commit,
203
+ params: parent, map: original.fetch(:map))
204
+ selected = CampaignExecution.validate_parent!(definition.review_campaign)
205
+ unless selected.values_at("campaign_id", "subject", "contract_identity") ==
206
+ execution.values_at("campaign_id", "subject", "contract_identity")
207
+ raise AttemptErrors::EvidenceUnavailable, "campaign child original parent differs"
208
+ end
209
+ {params: parent, map: original.fetch(:map), definition: definition, selected: selected}.freeze
210
+ end
211
+
212
+ def campaign_prepared_candidate!(journal:, commit:, execution:, map:)
213
+ original = campaign_original_parent_context!(journal, commit, execution, map)
214
+ CampaignExecution.validate!(execution, parent: original.fetch(:definition).id,
215
+ policy: original.fetch(:selected).fetch("policy"))
216
+ params = original.fetch(:params).merge("scope" => execution.fetch("parent_scope"),
217
+ "base" => execution.fetch("base"), "head" => execution.fetch("head"),
218
+ "candidate_generation" => execution.fetch("parent_candidate_generation"))
219
+ campaign_parent_candidate!(journal: journal, commit: commit, params: params, map: original.fetch(:map))
220
+ events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: original.fetch(:map))
221
+ selected = exact_candidate!(candidate(events), params)
222
+ bytes = journal.bounded_blob(selected.fetch("bundle_ref"), commit: commit, max_bytes: CandidateTransfer::MAX_BYTES)
223
+ unless bytes.is_a?(String) && bytes.bytesize == selected.fetch("bytes") &&
224
+ Digest::SHA256.hexdigest(bytes) == selected.fetch("sha256")
225
+ raise AttemptErrors::EvidenceUnavailable, "campaign original candidate bundle differs"
226
+ end
227
+ [selected.slice("head", "tree", "bytes", "sha256", "candidate_generation").merge("campaign_execution" => execution), bytes]
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end