ace-assign 0.70.0 → 0.73.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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +3 -2099
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -1
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
  35. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  36. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  37. data/lib/ace/assign/version.rb +1 -1
  38. data/lib/ace/assign.rb +0 -6
  39. metadata +4 -161
  40. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  41. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  42. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  43. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  44. data/docs/protected-authority.md +0 -282
  45. data/exe/ace-scope-ready +0 -55
  46. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  47. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  52. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  53. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  54. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  55. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  56. data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
  57. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  58. data/lib/ace/assign/authority/client.rb +0 -257
  59. data/lib/ace/assign/authority/deployment.rb +0 -627
  60. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  61. data/lib/ace/assign/authority/endcap.rb +0 -455
  62. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  63. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  64. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  65. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  66. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  67. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  68. data/lib/ace/assign/authority/endcap_publication.rb +0 -207
  69. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  70. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  71. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  72. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  73. data/lib/ace/assign/authority/endcap_services.rb +0 -598
  74. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  75. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  76. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  77. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  78. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  79. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  80. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  81. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  82. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  83. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  84. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  85. data/lib/ace/assign/authority/launch_review.rb +0 -91
  86. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  87. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  88. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  89. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  90. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  91. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  92. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  93. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  94. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  95. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  96. data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
  97. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  98. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
  99. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  100. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  101. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  102. data/lib/ace/assign/authority/ordinary_results.rb +0 -196
  103. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  104. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  105. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  106. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  107. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  108. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  109. data/lib/ace/assign/authority/prepared_work.rb +0 -304
  110. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  111. data/lib/ace/assign/authority/private_directory.rb +0 -33
  112. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  113. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  114. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  115. data/lib/ace/assign/authority/router.rb +0 -81
  116. data/lib/ace/assign/authority/server.rb +0 -131
  117. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  118. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  119. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  120. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  121. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  122. data/lib/ace/assign/authority/socket_server.rb +0 -255
  123. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  124. data/lib/ace/assign/authority/transfer_codec.rb +0 -187
  125. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  126. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  127. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  128. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  129. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  130. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  131. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  132. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  133. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  134. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  135. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  136. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  137. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  138. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  139. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  140. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  141. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  142. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  143. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  144. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  145. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  146. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  147. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  148. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  149. data/lib/ace/assign/models/attempt.rb +0 -178
  150. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  151. data/lib/ace/assign/models/evidence_event.rb +0 -86
  152. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  153. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  154. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  155. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  156. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
  157. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  158. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  159. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  160. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
  161. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  162. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  163. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  164. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  165. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  166. data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
  167. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  168. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  169. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  170. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  171. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  172. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  173. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  174. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  175. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
  176. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  177. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
  178. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  179. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  180. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  181. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- class Endcap
7
- private
8
-
9
- # Historical assignment lookup stays separate: revocation removes
10
- # permission, never the original receipt or response.
11
- def active_review_reservation(events, current)
12
- reservation = events.reverse.find do |event|
13
- event["type"] == "authority_mutation" &&
14
- %w[request_review assign_review].include?(event.dig("payload", "operation")) &&
15
- !event.dig("payload", "mutation_id").to_s.start_with?("review-delegate.") &&
16
- event.dig("payload", "data", "head") == current.fetch("head") &&
17
- event.dig("payload", "data", "candidate_generation") == current.fetch("candidate_generation")
18
- end
19
- return unless reservation
20
- cancelled = events.any? do |event|
21
- event["type"] == "authority_mutation" && event.dig("payload", "operation") == "cancel_review" &&
22
- event.dig("payload", "data", "review_event_id") == reservation.fetch("digest")
23
- end
24
- reservation unless cancelled
25
- end
26
-
27
- def active_review_event(events, current)
28
- reservation = active_review_reservation(events, current)
29
- return unless reservation
30
- return reservation if reservation.dig("payload", "operation") == "assign_review"
31
- events.find do |event|
32
- event["type"] == "authority_mutation" && event.dig("payload", "operation") == "assign_review" &&
33
- event.dig("payload", "mutation_id") == "review-delegate.#{reservation.fetch('digest')}"
34
- end
35
- end
36
-
37
- # Reserved spelling is not authorization. Reconstruct the immutable
38
- # request through the original-launcher reader before historical replay,
39
- # then separately check the active reservation for a fresh mutation.
40
- def delegated_review_request!(journal, events, request, params, peer, role)
41
- mutation = request.fetch("mutation_id")
42
- return unless mutation.is_a?(String) && mutation.start_with?("review-delegate.")
43
- digest = mutation.delete_prefix("review-delegate.")
44
- original = events.find { |event| event["digest"] == digest && event["type"] == "authority_mutation" &&
45
- event.dig("payload", "operation") == "request_review" }
46
- raise AttemptErrors::UnauthorizedIdentity, "delegation requires a canonical request" unless original
47
- accepted = journal.mutation_result(original.dig("payload", "mutation_id"))
48
- selectors = params.slice("mapping_id", "assignment_id", "attempt_id").merge(
49
- "mutation_id" => original.dig("payload", "mutation_id"), "request_event_id" => digest,
50
- "journal_commit" => accepted.fetch("journal_commit"),
51
- "original_binding_digest" => original.dig("payload", "data", "original_binding_digest"))
52
- intent = @launch.launch_review_intent!(request: request.merge("operation" => "launch_review_intent",
53
- "mutation_id" => nil, "params" => selectors), peer: peer, role: role)
54
- unless Atoms::EvidenceDigest.digest(intent.dig(:data, "assignment_params")) == Atoms::EvidenceDigest.digest(params)
55
- raise AttemptErrors::UnauthorizedIdentity, "delegation differs from exact canonical request"
56
- end
57
- original
58
- end
59
-
60
- def cancel_review(request, params, map, peer, role, transfer)
61
- raise ArgumentError, "review cancellation forbids upload" unless transfer.nil?
62
- unless params["review_event_id"].is_a?(String) && params["review_event_id"].match?(/\A[0-9a-f]{64}\z/)
63
- raise ArgumentError, "review cancellation selector differs"
64
- end
65
- @launch.with_assignment(params: params, map: map) do |journal, _registration|
66
- protected_journal!(journal)
67
- events = attempt_events(journal, params)
68
- authenticate_review_cancellation!(events, params, map, peer, role)
69
- result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
70
- mutation_id: request.fetch("mutation_id"), operation: "cancel_review",
71
- parameters_digest: Atoms::EvidenceDigest.digest(params), expected_generation: params.fetch("expected_generation"),
72
- with_replay: true) do |current_events, _commit, _generation|
73
- authenticate_review_cancellation!(current_events, params, map, peer, role)
74
- current = exact_candidate!(candidate(current_events), params)
75
- reservation = active_review_reservation(current_events, current)
76
- unless reservation && reservation.fetch("digest") == params.fetch("review_event_id")
77
- raise AttemptErrors::Conflict, "review reservation is no longer active"
78
- end
79
- {data: current.slice("head", "candidate_generation").merge(
80
- "review_event_id" => reservation.fetch("digest"),
81
- "review_id" => active_review_event(current_events, current)&.dig("payload", "data", "review_id"), "state" => "cancelled")}
82
- end
83
- review_event_reply(journal, result, request, params, "cancellation_event_id")
84
- end
85
- end
86
-
87
- def review_event_reply(journal, result, request, params, field)
88
- accepted = journal.read_events(params.fetch("assignment_id"), commit: result.fetch(:data).fetch("journal_commit")).find do |event|
89
- event["attempt_id"] == params.fetch("attempt_id") && event["type"] == "authority_mutation" &&
90
- event.dig("payload", "operation") == request.fetch("operation") &&
91
- event.dig("payload", "mutation_id") == request.fetch("mutation_id")
92
- end
93
- raise AttemptErrors::EvidenceUnavailable, "accepted review event is unavailable" unless accepted
94
- result.merge(data: result.fetch(:data).merge(field => accepted.fetch("digest")))
95
- end
96
-
97
- def authenticate_review_cancellation!(events, params, map, peer, role)
98
- origin = active_origin(events, params)
99
- @kernel.live!(peer)
100
- original = events.find { |event| event["digest"] == params.fetch("review_event_id") }
101
- unless original && original["type"] == "authority_mutation" &&
102
- %w[request_review assign_review].include?(original.dig("payload", "operation")) &&
103
- !original.dig("payload", "mutation_id").to_s.start_with?("review-delegate.") &&
104
- original.dig("payload", "data", "head") == params.fetch("head") &&
105
- original.dig("payload", "data", "candidate_generation") == params.fetch("candidate_generation")
106
- raise AttemptErrors::UnauthorizedIdentity, "review cancellation requires the exact reservation root"
107
- end
108
- if role == :launcher
109
- unless @kernel.same?(origin.fetch("launcher_identity"), peer)
110
- raise AttemptErrors::UnauthorizedIdentity, "cancellation requires original launcher birth"
111
- end
112
- worker_or_launcher!(peer, role, map, origin, launcher_only: true)
113
- elsif role == :reviewer && original.dig("payload", "operation") == "request_review"
114
- project = @deployment.project(map.fetch("project_id"))
115
- credentials = project.fetch("peer_credentials")[peer.fetch("uid").to_s]
116
- principal = peer.slice("uid", "gid", "groups").merge("role" => "reviewer")
117
- unless project.fetch("reviewer_uids").include?(peer.fetch("uid")) && peer.fetch("uid") != map.fetch("worker_uid") &&
118
- credentials && peer.values_at("gid", "groups") == credentials.values_at("gid", "groups") &&
119
- Atoms::EvidenceDigest.digest(principal) == Atoms::EvidenceDigest.digest(original.dig("payload", "data", "requester"))
120
- raise AttemptErrors::UnauthorizedIdentity, "cancellation requires the original mapped requester"
121
- end
122
- else
123
- raise AttemptErrors::UnauthorizedIdentity, "review reservation cancellation is unauthorized"
124
- end
125
- original
126
- end
127
- end
128
- end
129
- end
130
- end
@@ -1,152 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- class Endcap
7
- private
8
-
9
- def authorize_service_settlement!(request, params, map, peer, role)
10
- @launch.with_assignment(params: params, map: map) do |journal, _registration|
11
- protected_journal!(journal)
12
- service_settlement_record!(journal, params, map, peer, role)
13
- end
14
- true
15
- end
16
-
17
- def service_settlement_record!(journal, params, map, peer, role, record: nil)
18
- record ||= journal.service_request(params.fetch("request_id"))
19
- raise AttemptErrors::NotFound, "canonical service request is missing" unless record
20
- service_executor!(peer, role, record)
21
- receiver = service_receiver!(peer, role, map, record.fetch("service_id"))
22
- credentials = @deployment.project(map.fetch("project_id")).fetch("peer_credentials").fetch(peer.fetch("uid").to_s)
23
- unless peer["gid"] == credentials.fetch("gid") && peer["groups"] == credentials.fetch("groups") &&
24
- receiver.fetch("executor_uid") == record.fetch("executor_uid") &&
25
- %w[assignment_id attempt_id mapping_id].all? { |key| record[key] == params[key] } &&
26
- record["project_id"] == map.fetch("project_id") && record["candidate_head"] == params.fetch("head") &&
27
- record["candidate_generation"] == params.fetch("candidate_generation") &&
28
- %w[issued dispatch_started].include?(record["dispatch_phase"])
29
- raise AttemptErrors::UnauthorizedIdentity, "original service settlement identity differs"
30
- end
31
- # The normal protected binding validates original ticket/claim, even
32
- # while effect policy, worker liveness or current candidate changed.
33
- ServiceEvidence.new(journal: journal).context(record)
34
- record
35
- rescue KeyError
36
- raise AttemptErrors::EvidenceUnavailable, "original service settlement binding is incomplete"
37
- end
38
-
39
- def dispatch_service_settlement(request, params, map, peer, role, transfer)
40
- authorize_service_settlement!(request, params, map, peer, role)
41
- completion = request.fetch("operation") == "complete_no_effect"
42
- admitted = completion && ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"),
43
- artifact_field: "evidence", reference_key: "ref")
44
- @launch.with_assignment(params: params, map: map) do |journal, _registration|
45
- record = service_settlement_record!(journal, params, map, peer, role)
46
- no_effect_completion_binding!(record, params, admitted) if completion
47
- journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
48
- mutation_id: request.fetch("mutation_id"), operation: request.fetch("operation"),
49
- parameters_digest: Atoms::EvidenceDigest.digest(params),
50
- expected_generation: completion ? nil : params.fetch("expected_generation"),
51
- generation_mode: completion ? :recorded_completion : :expected, with_replay: true) do |events, commit, generation|
52
- current = service_settlement_record!(journal, params, map, peer, role)
53
- if completion
54
- no_effect_completion_plan(journal, events, current, params, admitted)
55
- else
56
- service_challenge_plan(journal, events, current, params, map, generation, commit)
57
- end
58
- end
59
- end
60
- end
61
-
62
- def service_challenge_projection(record)
63
- record.slice("request_id", "state", "dispatch_ticket_id", "claim_binding", "dispatch_phase").merge(
64
- "reconciliation_challenge" => %w[succeeded failed-settled].include?(record.fetch("state")) ? nil :
65
- record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest"))
66
- end
67
-
68
- def service_challenge_plan(journal, events, record, params, map, generation, commit)
69
- lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
70
- assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
71
- unless lineage.sealed? && lineage.binding && record["reservation_generation"] == lineage.binding.fetch("reservation_generation")
72
- raise AttemptErrors::Conflict, "original service generation is not sealed"
73
- end
74
- return {data: service_challenge_projection(record)} if %w[succeeded failed-settled].include?(record.fetch("state"))
75
- unless %w[uncertain failed].include?(record.fetch("state"))
76
- raise AttemptErrors::EvidenceUnavailable, "original protected request has no uncertainty outcome"
77
- end
78
- if record["challenge_event_digest"]
79
- begin
80
- ServiceEvidence.new(journal: journal).challenge!(record, pending: {commit: commit})
81
- return {data: service_challenge_projection(record)}
82
- rescue AttemptErrors::EvidenceUnavailable
83
- # Only a positively authenticated later failure may supersede a
84
- # challenge. Corrupt/missing old history never becomes freshness.
85
- old = ServiceEvidence.new(journal: journal).challenge!(record, pending: {commit: commit}, current: false)
86
- raise unless events.drop_while { |event| event["digest"] != old.fetch("digest") }.drop(1).any? do |event|
87
- %w[service_claim service_transition].include?(event["type"]) && event.dig("payload", "request_id") == record.fetch("request_id") &&
88
- %w[uncertain failed].include?(event.dig("payload", "state"))
89
- end
90
- end
91
- end
92
- failure = events.reverse.find { |event| %w[service_claim service_transition].include?(event["type"]) &&
93
- event.dig("payload", "request_id") == record.fetch("request_id") && %w[uncertain failed].include?(event.dig("payload", "state")) }
94
- raise AttemptErrors::EvidenceUnavailable, "canonical service failure is missing" unless failure
95
- failure_prefix = events.take_while { |event| event["digest"] != failure.fetch("digest") }
96
- payload = record.slice("request_id", "input_digest", "claim_binding", "dispatch_ticket_id").merge(
97
- "version" => 1, "no_effect_challenge" => SecureRandom.hex(16), "challenge_generation" => generation + 1,
98
- "failure_event_digest" => failure.fetch("digest"), "failure_generation" => journal.authority_generation(failure_prefix) + 1)
99
- recorded_at = Time.now.utc
100
- challenge = Models::EvidenceEvent.build(type: "service_no_effect_challenge", attempt_id: params.fetch("attempt_id"),
101
- payload: payload, previous_digest: events.last&.fetch("digest"), recorded_at: recorded_at)
102
- replacement = record.merge(payload.slice("no_effect_challenge", "challenge_generation"), "challenge_event_digest" => challenge.fetch("digest"))
103
- {events: [{type: "service_no_effect_challenge", payload: payload, recorded_at: recorded_at}],
104
- data: service_challenge_projection(replacement), service_updates: [{request_id: record.fetch("request_id"),
105
- expected: record, replacement: replacement, event_type: "service_challenge"}]}
106
- end
107
-
108
- def no_effect_completion_binding!(record, params, admitted)
109
- selector = params.fetch("reconciliation_challenge")
110
- unless selector.is_a?(Hash) && selector.keys.sort == %w[no_effect_challenge challenge_generation challenge_event_digest].sort &&
111
- selector["no_effect_challenge"].is_a?(String) && selector["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
112
- selector["challenge_generation"].is_a?(Integer) && selector["challenge_generation"].positive? &&
113
- selector["challenge_event_digest"].is_a?(String) && selector["challenge_event_digest"].match?(/\A[0-9a-f]{64}\z/)
114
- raise ArgumentError, "no-effect challenge selector is malformed"
115
- end
116
- unless selector.all? { |key, value| record[key] == value } && record["claim_binding"] == params.fetch("claim_binding")
117
- raise AttemptErrors::Conflict, "no-effect challenge or claim differs"
118
- end
119
- receipt = admitted.fetch(:receipt)
120
- unless receipt.keys.sort == Molecules::EvidenceJournal::TERMINAL_RECEIPT_FIELDS.sort && receipt["outcome"] == "failed" &&
121
- Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS.all? { |field| receipt[field] == record[field] }
122
- raise AttemptErrors::ReceiptRejected, "no-effect receipt original binding differs"
123
- end
124
- digest = Atoms::EvidenceDigest.digest("challenge" => selector, "receipt_sha256" => admitted.fetch(:receipt_sha256),
125
- "artifacts" => admitted.fetch(:artifacts).map { |bytes| Digest::SHA256.hexdigest(bytes) })
126
- if record["state"] == "succeeded" || record["no_effect_completion_digest"] && record["no_effect_completion_digest"] != digest
127
- raise AttemptErrors::Conflict, "no-effect completion contradicts retained outcome"
128
- end
129
- digest
130
- end
131
-
132
- def no_effect_completion_plan(journal, events, record, params, admitted)
133
- digest = no_effect_completion_binding!(record, params, admitted)
134
- return {data: service_projection(record)} if record["no_effect_completion_digest"]
135
- owner = ServiceEvidence.new(journal: journal)
136
- challenge = owner.challenge!(record)
137
- if record["operation"] == "prune-preserved-workspace"
138
- owner.cleanup_inspection_inputs!(admitted.fetch(:artifacts), record, challenge)
139
- else
140
- admitted.fetch(:artifacts).each { |bytes| owner.inspection!(bytes, record, challenge) }
141
- end
142
- plan = Molecules::CanonicalEvidence.new(journal: journal).import_plan(**owner.context(record, no_effect: true),
143
- artifacts: admitted.fetch(:artifacts), admitted_after_event_digest: challenge.fetch("digest"))
144
- receipt = admitted.fetch(:receipt).merge("evidence" => plan.fetch(:references))
145
- replacement = record.merge("state" => "failed-settled", "receipt" => receipt, "no_effect_completion_digest" => digest)
146
- plan.merge(data: service_projection(replacement), service_updates: [{request_id: record.fetch("request_id"),
147
- expected: record, replacement: replacement, event_type: "service_transition"}])
148
- end
149
- end
150
- end
151
- end
152
- end