ace-assign 0.71.0 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) 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 +5 -2116
  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 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  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 +6 -2
  35. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
  36. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  37. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  38. data/lib/ace/assign/version.rb +1 -1
  39. data/lib/ace/assign.rb +0 -6
  40. metadata +22 -176
  41. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  42. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  43. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  44. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  45. data/docs/protected-authority.md +0 -282
  46. data/exe/ace-scope-ready +0 -55
  47. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  48. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  49. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  50. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  51. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  52. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  53. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  54. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  55. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  56. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  57. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  58. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  59. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  60. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  61. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  62. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  63. data/lib/ace/assign/authority/client.rb +0 -257
  64. data/lib/ace/assign/authority/command_execution.rb +0 -180
  65. data/lib/ace/assign/authority/deployment.rb +0 -627
  66. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  67. data/lib/ace/assign/authority/endcap.rb +0 -457
  68. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  69. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  70. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  71. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  72. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  73. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  74. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  75. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  76. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  77. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  78. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  79. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  80. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  81. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  82. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  83. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  84. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  85. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  86. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  87. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  88. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  89. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  90. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  91. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  92. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  93. data/lib/ace/assign/authority/launch_review.rb +0 -91
  94. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  95. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  96. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  97. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  98. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  99. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  100. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  101. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  102. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  103. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  104. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  105. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  106. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  107. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  108. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  109. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  110. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  111. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  112. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  113. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  114. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  115. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  116. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  117. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  118. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  119. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  120. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  121. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  122. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  123. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  124. data/lib/ace/assign/authority/private_directory.rb +0 -33
  125. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  126. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  127. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  128. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  129. data/lib/ace/assign/authority/retained_input.rb +0 -53
  130. data/lib/ace/assign/authority/router.rb +0 -81
  131. data/lib/ace/assign/authority/server.rb +0 -131
  132. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  133. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  134. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  135. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  136. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  137. data/lib/ace/assign/authority/socket_server.rb +0 -255
  138. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  139. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  140. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  141. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  142. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  143. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  144. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  145. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  146. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  147. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  148. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  149. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  151. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  152. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  153. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  154. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  155. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  156. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  157. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  158. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  159. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  160. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  161. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  162. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  163. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  164. data/lib/ace/assign/models/attempt.rb +0 -178
  165. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  166. data/lib/ace/assign/models/evidence_event.rb +0 -86
  167. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  168. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  169. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  170. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  171. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  172. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  173. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  174. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  175. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  176. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  177. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  178. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  179. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  180. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  181. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  182. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  183. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  184. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  185. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  186. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  187. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  188. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  189. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  190. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  191. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  192. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  193. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  194. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  195. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  196. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  197. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,594 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- module FixedServiceLifecycle
7
- SERVICE_IDENTITY = %w[request_id assignment_id attempt_id project_id operation input_digest target
8
- authorization service_id caller_uid candidate_head candidate_generation executor_uid transport policy_digest
9
- worker_process_binding launch_ticket reservation_generation mapping_id].freeze
10
-
11
- # Source-owned terminal admission. Caller already owns lifecycle/journal
12
- # exclusion; the immutable commit is the current CAS input, never a
13
- # caller-selected alternate evidence ref.
14
- def service_settlement_complete!(journal:, events:, params:, map:, commit:)
15
- service_settlement_evidence!(journal: journal, events: events, params: params, map: map, commit: commit)
16
- true
17
- end
18
-
19
- def service_settlement_evidence!(journal:, events:, params:, map:, commit:)
20
- service_journal!(journal)
21
- journal.verify_commit!(commit)
22
- retained = journal.read_events(params.fetch("assignment_id"), commit: commit)
23
- .select { |event| event["attempt_id"] == params.fetch("attempt_id") }
24
- unless retained == events && Models::EvidenceEvent.chain_valid?(events)
25
- raise AttemptErrors::EvidenceUnavailable, "service settlement requires exact canonical commit"
26
- end
27
- references = events.select { |event| %w[service_claim service_transition].include?(event["type"]) }
28
- .map { |event| event.fetch("payload").fetch("request_id") }.uniq.sort
29
- records = journal.service_request_records(commit: commit).select do |record|
30
- record["assignment_id"] == params.fetch("assignment_id") && record["attempt_id"] == params.fetch("attempt_id")
31
- end
32
- unless records.map { |record| record.fetch("request_id") }.uniq.sort == references
33
- raise AttemptErrors::EvidenceUnavailable, "canonical service request set is incomplete"
34
- end
35
- pending = false
36
- projected = records.sort_by { |record| record.fetch("request_id") }.map do |record|
37
- unless record["project_id"] == map.fetch("project_id") && record["mapping_id"] == params.fetch("mapping_id")
38
- raise AttemptErrors::EvidenceUnavailable, "canonical service request is not settled for this scope"
39
- end
40
- # This existing reader verifies the accepted record digest and all
41
- # imported terminal/no-effect evidence against this exact commit.
42
- verified = journal.service_request(record.fetch("request_id"), commit: commit)
43
- unless verified == record && %w[accepted uncertain rejected succeeded failed failed-settled].include?(verified.fetch("state"))
44
- raise AttemptErrors::EvidenceUnavailable, "canonical service record differs"
45
- end
46
- if %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| verified.key?(key) }
47
- ServiceEvidence.new(journal: journal).challenge!(verified, pending: {commit: commit}, current: false)
48
- end
49
- unless %w[succeeded failed-settled].include?(verified.fetch("state"))
50
- pending = true
51
- next
52
- end
53
- transition = events.reverse.find { |event| %w[service_claim service_transition].include?(event["type"]) &&
54
- event.dig("payload", "request_id") == record.fetch("request_id") }
55
- receipt = verified.fetch("receipt")
56
- unless transition && transition["type"] == "service_transition" &&
57
- transition.dig("payload", "state") == verified.fetch("state") &&
58
- transition.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(verified) &&
59
- transition.dig("payload", "receipt_digest") == Atoms::EvidenceDigest.digest(receipt)
60
- raise AttemptErrors::EvidenceUnavailable, "accepted service terminal transition differs"
61
- end
62
- {"request_id" => verified.fetch("request_id"), "state" => verified.fetch("state"),
63
- "event_digest" => transition.fetch("digest"), "record_digest" => transition.dig("payload", "record_digest"),
64
- "receipt_digest" => transition.dig("payload", "receipt_digest"),
65
- "evidence_refs" => receipt.fetch("evidence").sort_by { |ref| ref.values_at("ref", "sha256") }}
66
- end
67
- projected.compact!
68
- unless projected.empty?
69
- journal.event_commits!(assignment_id: params.fetch("assignment_id"),
70
- event_digests: projected.map { |row| row.fetch("event_digest") }, commit: commit)
71
- end
72
- if pending
73
- raise AttemptErrors::ServiceSettlementPending, "authenticated canonical service requests remain unsettled"
74
- end
75
- settlement_immutable({"commit" => commit, "services" => projected})
76
- rescue KeyError, TypeError, ArgumentError, NoMethodError, AttemptErrors::ReceiptRejected
77
- raise AttemptErrors::EvidenceUnavailable, "canonical service settlement is unverifiable"
78
- end
79
-
80
- # Called only by the installed journal composition, including lower
81
- # service writers. It never reacquires lifecycle/authority locks.
82
- def authorize_service_update!(journal:, existing:, replacement:, pending:)
83
- service_journal!(journal)
84
- if existing && existing.key?("executor_process_binding") &&
85
- Atoms::EvidenceDigest.digest(existing.fetch("executor_process_binding")) != Atoms::EvidenceDigest.digest(replacement["executor_process_binding"])
86
- raise AttemptErrors::InvalidState, "original cleanup executor cannot change"
87
- end
88
- if existing && existing.key?("operation_owner_binding") &&
89
- Atoms::EvidenceDigest.digest(existing.fetch("operation_owner_binding")) != Atoms::EvidenceDigest.digest(replacement["operation_owner_binding"])
90
- raise AttemptErrors::InvalidState, "original cleanup dispatch owner cannot change"
91
- end
92
- if existing && %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| existing[key] != replacement[key] }
93
- unless pending && pending[:operation] == "claim_service_settlement" &&
94
- existing["state"] == replacement["state"] && existing["receipt"] == replacement["receipt"] &&
95
- existing["dispatch_phase"] == replacement["dispatch_phase"]
96
- raise AttemptErrors::InvalidState, "no-effect challenge requires its fixed source owner"
97
- end
98
- ServiceEvidence.new(journal: journal).challenge!(replacement, pending: pending)
99
- end
100
- initial = existing.nil? && %w[accepted uncertain].include?(replacement["state"])
101
- if initial && (replacement.key?("operation_owner_binding") || replacement.key?("executor_process_binding"))
102
- raise AttemptErrors::InvalidState, "cleanup owner identity requires fresh dispatch admission"
103
- end
104
- begin_dispatch = existing && existing["dispatch_phase"] == "issued" && replacement["dispatch_phase"] == "dispatch_started"
105
- if existing && existing["operation"] == "publish" && pending && %w[publication_challenge publication_continue].include?(pending[:operation])
106
- return authorize_publication_update!(journal: journal, existing: existing, replacement: replacement, pending: pending)
107
- end
108
- if existing && existing["dispatch_phase"] != replacement["dispatch_phase"] && !begin_dispatch
109
- raise AttemptErrors::InvalidState, "invalid protected dispatch phase change"
110
- end
111
- return true unless initial || begin_dispatch
112
- unless replacement["state"] == "uncertain" && (initial ? replacement["dispatch_phase"] == "issued" : true)
113
- raise AttemptErrors::InvalidState, "fresh protected effect requires an uncertain dispatch ticket"
114
- end
115
- bytes = pending && pending.fetch(:service_inputs, {})[replacement.fetch("request_id")]
116
- raise AttemptErrors::UnauthorizedIdentity, "fresh service effect requires its exact original input" unless bytes.is_a?(String)
117
- map = service_mapping!(replacement.fetch("mapping_id"))
118
- unless replacement["project_id"] == map["project_id"] && replacement["caller_uid"] == map["worker_uid"]
119
- raise AttemptErrors::UnauthorizedIdentity, "protected service mapping differs"
120
- end
121
- params = replacement.slice("mapping_id", "assignment_id", "attempt_id", "candidate_generation").merge("head" => replacement.fetch("candidate_head"))
122
- events = pending.fetch(:current_events)
123
- service_scope_open!(events: events, params: params, map: map)
124
- origin = service_origin!(events, params)
125
- unless service_origin_matches!(replacement, origin)
126
- raise AttemptErrors::UnauthorizedIdentity, "protected service origin differs"
127
- end
128
- receiver = service_project!(map.fetch("project_id")).fetch("service_receivers").fetch(replacement.fetch("service_id"))
129
- raise AttemptErrors::UnauthorizedIdentity, "protected service executor differs" unless receiver.fetch("executor_uid") == replacement.fetch("executor_uid")
130
- current = exact_candidate!(candidate(events), params)
131
- service_review!(journal, events, params, map, current, service_operation: replacement.fetch("operation")) unless %w[create update].include?(replacement.fetch("operation"))
132
- service_policy!.prepare!(replacement, input_bytes: bytes)
133
- if begin_dispatch && %w[prune-preserved-workspace publish].include?(replacement.fetch("operation"))
134
- executor = replacement.fetch("executor_process_binding")
135
- boot = replacement.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
136
- Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
137
- credentials = service_project!(map.fetch("project_id")).fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
138
- unless executor.slice("uid", "gid", "groups") == credentials.slice("gid", "groups").merge("uid" => receiver.fetch("executor_uid"))
139
- raise AttemptErrors::UnauthorizedIdentity, "original cleanup executor credentials differ"
140
- end
141
- @kernel.live!(executor)
142
- unless replacement["operation"] != "prune-preserved-workspace" || Atoms::EvidenceDigest.digest(service_policy!.dispatch_owner_binding!(replacement)) ==
143
- Atoms::EvidenceDigest.digest(replacement.fetch("operation_owner_binding"))
144
- raise AttemptErrors::UnauthorizedIdentity, "fixed cleanup owner changed before dispatch acceptance"
145
- end
146
- end
147
- true
148
- rescue KeyError
149
- raise AttemptErrors::UnauthorizedIdentity, "protected service admission context is incomplete"
150
- end
151
-
152
- private
153
-
154
- def settlement_immutable(value)
155
- case value
156
- when Hash then value.each_with_object({}) { |(key, item), result| result[key.dup.freeze] = settlement_immutable(item) }.freeze
157
- when Array then value.map { |item| settlement_immutable(item) }.freeze
158
- when String then value.dup.freeze
159
- else value.freeze
160
- end
161
- end
162
-
163
- def authorize_service_transfer!(request, params, map, peer, role)
164
- service_policy!
165
- service_policy!.visible!(project: map.fetch("project_id"), uid: map.fetch("worker_uid")) unless request.fetch("operation") == "complete_service"
166
- with_service_scope(params: params, map: map) do |journal, _registration|
167
- service_journal!(journal)
168
- if request.fetch("operation") == "request_service"
169
- existing = journal.service_request(params.fetch("request_id"))
170
- if existing
171
- service_executor!(peer, role, existing)
172
- service_replay_binding!(existing, params, map)
173
- else
174
- events = attempt_events(journal, params)
175
- origin = service_origin!(events, params)
176
- service_worker!(params.fetch("worker_process_binding"), :worker, map, origin)
177
- service_receiver!(peer, role, map, params.fetch("service_id"))
178
- end
179
- else
180
- record = journal.service_request(params.fetch("request_id"))
181
- raise AttemptErrors::NotFound, "service request is unavailable" unless record
182
- service_executor!(peer, role, record)
183
- service_ticket!(record, params, map, peer)
184
- end
185
- end
186
- true
187
- end
188
-
189
- # Receiver identity and forwarded worker identity are independent checks.
190
- # This immutable observation grants no service claim or dispatch permit.
191
- def workspace_prune_preview_context(_request, params, map, peer, role, transfer)
192
- raise ArgumentError, "preview context forbids upload" unless transfer.nil?
193
- with_service_scope(params: params, map: map) do |journal, _registration|
194
- {data: workspace_prune_preview_context_at!(journal: journal, params: params, map: map, peer: peer, role: role), replayed: false}
195
- end
196
- end
197
-
198
- # Only source composition supplies this fixed journal/snapshot owner.
199
- # Both ordinary admission and the root read-only view use the same
200
- # original registration, process and candidate proof implementation.
201
- public
202
-
203
- def workspace_prune_preview_context_at!(journal:, params:, map:, peer:, role:)
204
- service_policy!.visible!(project: map.fetch("project_id"), uid: map.fetch("worker_uid"))
205
- service_journal!(journal)
206
- commit = journal.ref_value
207
- events = service_preview_events!(journal: journal, commit: commit, params: params, map: map)
208
- origin = service_origin!(events, params)
209
- service_worker!(params.fetch("worker_process_binding"), :worker, map, origin)
210
- receiver = service_receiver!(peer, role, map, params.fetch("service_id"))
211
- service_policy!.workspace_prune_receiver!(project: map.fetch("project_id"), uid: map.fetch("worker_uid"),
212
- service_id: params.fetch("service_id"), executor_uid: receiver.fetch("executor_uid"))
213
- selected = candidate(events)
214
- unless selected && selected["head"].is_a?(String) && selected["head"].match?(CandidateTransfer::SHA) &&
215
- selected["candidate_generation"].is_a?(Integer) && selected["candidate_generation"].positive?
216
- raise AttemptErrors::EvidenceUnavailable, "preview candidate is unavailable"
217
- end
218
- data = {
219
- "journal_commit" => commit,
220
- "maintenance" => params.slice("mapping_id", "assignment_id", "attempt_id").merge("project_id" => map.fetch("project_id")),
221
- "head" => selected.fetch("head"), "candidate_generation" => selected.fetch("candidate_generation"),
222
- "worker_process_binding" => service_origin_fields!(origin).fetch("worker_process_binding"),
223
- "caller_process_binding" => params.fetch("worker_process_binding")
224
- }
225
- raise AttemptErrors::Conflict, "preview canonical selection changed" unless journal.ref_value == commit
226
- raise AttemptErrors::EvidenceUnavailable, "preview context exceeds bound" if JSON.generate(data).bytesize > 16_384
227
- Atoms::EvidenceDigest.digest(data) # Reject unsupported transport values before copying.
228
- JSON.parse(JSON.generate(data))
229
- end
230
-
231
- private
232
-
233
- def service_status(_request, params, map, peer, role)
234
- with_service_scope(params: params, map: map) do |journal, _registration|
235
- service_journal!(journal)
236
- commit = journal.ref_value
237
- read = role == :executor && journal.service_settlement_read(params.fetch("request_id"), commit: commit)
238
- record = read ? read.fetch(:record) : journal.service_request(params.fetch("request_id"), commit: commit)
239
- unless record && record["assignment_id"] == params["assignment_id"] && record["attempt_id"] == params["attempt_id"] &&
240
- record["project_id"] == map["project_id"] && record["candidate_head"] == params["head"] &&
241
- record["candidate_generation"] == params["candidate_generation"]
242
- raise AttemptErrors::UnauthorizedIdentity, "service status binding differs"
243
- end
244
- if role != :executor && %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| record.key?(key) }
245
- ServiceEvidence.new(journal: journal).challenge!(record, pending: {commit: commit}, current: false)
246
- end
247
- selected = read ? read.fetch(:read_view).inventory.fetch("events").fetch(params.fetch("assignment_id")) :
248
- journal.read_events(params.fetch("assignment_id"), commit: commit)
249
- events = selected.select { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
250
- @kernel.live!(peer)
251
- service_policy!.visible!(project: map.fetch("project_id"), uid: peer.fetch("uid"))
252
- if role == :executor
253
- record = service_settlement_record!(journal, params, map, peer, role, record: record)
254
- else
255
- authorized = case role
256
- when :worker
257
- peer["uid"] == record["caller_uid"] && @kernel.descendant?(peer, record.fetch("worker_process_binding"))
258
- when :reviewer
259
- review = assigned_review(events)
260
- review && review["reviewer_uid"] == peer["uid"] && review["head"] == params["head"] &&
261
- review["candidate_generation"] == params["candidate_generation"] && @kernel.descendant?(peer, review.fetch("reviewer_process_binding"))
262
- when :supervisor
263
- service_project!(map.fetch("project_id")).fetch("supervisor_uids").include?(peer["uid"])
264
- else
265
- false
266
- end
267
- raise AttemptErrors::UnauthorizedIdentity, "service status purpose differs" unless authorized
268
- end
269
- data = service_projection(record).merge("generation" => journal.authority_generation(events),
270
- "journal_commit" => commit)
271
- data["input_digest"] = record.fetch("input_digest") if %w[create update ready merge].include?(record["operation"])
272
- if %w[create update ready merge].include?(record["operation"]) && record["state"] == "succeeded"
273
- event = ServiceDeliveryEvidence.new(journal: journal).verified(record: record, events: events, commit: commit)
274
- data.merge!(record.slice("input_digest", "authorization", "executor_uid", "completion_digest", "receipt"))
275
- data["delivery_event"] = event
276
- end
277
- if role == :executor
278
- data["settlement_context"] = read.fetch(:settlement_context) ||
279
- ServiceEvidence.new(journal: journal).settlement_context(record, commit: commit, read_view: read.fetch(:read_view))
280
- end
281
- {data: data, replayed: false}
282
- end
283
- end
284
-
285
- def service_authorization(request, params, map, peer, role, transfer)
286
- authorize_service_transfer!(request, params, map, peer, role)
287
- raise ArgumentError, "structured service input transfer is missing" unless transfer && transfer.count == 1
288
- bytes = transfer.bytes.dup.freeze
289
- with_service_scope(params: params, map: map) do |journal, _registration|
290
- service_journal!(journal)
291
- record = journal.service_request(params.fetch("request_id"))
292
- service_executor!(peer, role, record)
293
- service_ticket!(record, params, map, peer)
294
- unless record.fetch("input_digest") == params.fetch("input_digest")
295
- raise AttemptErrors::Conflict, "authorization input binding differs"
296
- end
297
- events = attempt_events(journal, params)
298
- service_scope_open!(events: events, params: params, map: map)
299
- origin = service_origin!(events, params)
300
- unless service_origin_matches!(record, origin)
301
- raise AttemptErrors::UnauthorizedIdentity, "authorization launch origin differs"
302
- end
303
- current = exact_candidate!(candidate(events), params)
304
- unless record["candidate_head"] == current["head"] && record["candidate_generation"] == current["candidate_generation"]
305
- raise AttemptErrors::Conflict, "authorization candidate changed"
306
- end
307
- service_receiver!(peer, role, map, record.fetch("service_id"))
308
- service_review!(journal, events, params, map, current, service_operation: record.fetch("operation")) unless %w[create update].include?(record.fetch("operation"))
309
- prepared = service_policy!.prepare!(record, input_bytes: bytes)
310
- data = params.slice("request_id", "claim_binding", "head", "candidate_generation").merge(
311
- "policy_digest" => prepared.fetch(:policy_digest), "operation_digest" => prepared.fetch(:operation_digest))
312
- {data: data, replayed: false}
313
- end
314
- end
315
-
316
- def dispatch_service(request, params, map, peer, role, transfer)
317
- return service_authorization(request, params, map, peer, role, transfer) if request.fetch("operation") == "service_authorization"
318
- authorize_service_transfer!(request, params, map, peer, role)
319
- admitted = if request.fetch("operation") == "complete_service"
320
- ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"), artifact_field: "evidence", reference_key: "ref")
321
- else
322
- raise ArgumentError, "structured service input transfer is missing" unless transfer && transfer.count == 1
323
- transfer.bytes.dup.freeze
324
- end
325
- with_service_scope(params: params, map: map) do |journal, _registration|
326
- service_journal!(journal)
327
- unless request.fetch("operation") == "complete_service"
328
- input_record = request.fetch("operation") == "request_service" ? params : journal.service_request(params.fetch("request_id"))
329
- # Replay bypasses the mutation callback, never input validation.
330
- # Only body binding is checked here; current effect policy stays
331
- # inside each fresh CAS callback.
332
- service_policy!.input_binding(admitted, expected_digest: input_record.fetch("input_digest"),
333
- expected_target: input_record.fetch("target"), operation: input_record.fetch("operation"))
334
- end
335
- # Admission also runs outside mutate: an exact mutation replay
336
- # skips its callback and cannot bypass the authenticated ticket.
337
- service_policy!.visible!(project: map.fetch("project_id"), uid: map.fetch("worker_uid")) unless request.fetch("operation") == "complete_service"
338
- retained = request.fetch("operation") == "request_service" && journal.service_request(params.fetch("request_id"))
339
- if retained
340
- service_executor!(peer, role, retained)
341
- service_replay_binding!(retained, params, map)
342
- end
343
- if !retained && request.fetch("operation") != "request_service"
344
- record = journal.service_request(params.fetch("request_id"))
345
- service_executor!(peer, role, record)
346
- service_ticket!(record, params, map, peer)
347
- completion_binding!(record, params, admitted) if request.fetch("operation") == "complete_service"
348
- elsif !retained
349
- origin = service_origin!(attempt_events(journal, params), params)
350
- service_worker!(params.fetch("worker_process_binding"), :worker, map, origin)
351
- service_receiver!(peer, role, map, params.fetch("service_id"))
352
- end
353
- result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
354
- mutation_id: request.fetch("mutation_id"), operation: request.fetch("operation"),
355
- parameters_digest: Atoms::EvidenceDigest.digest(params),
356
- expected_generation: request.fetch("operation") == "complete_service" ? nil : params.fetch("expected_generation"),
357
- generation_mode: request.fetch("operation") == "complete_service" ? :recorded_completion : :expected, with_replay: true) do |events, _commit, _generation|
358
- if request.fetch("operation") == "request_service"
359
- existing = journal.service_request(params.fetch("request_id"))
360
- if existing
361
- service_executor!(peer, role, existing)
362
- service_replay_binding!(existing, params, map)
363
- {data: service_projection(existing).merge("claim" => "retained")}
364
- else
365
- service_claim_plan(journal, events, params, map, peer, role, admitted)
366
- end
367
- elsif request.fetch("operation") == "begin_dispatch"
368
- service_begin_plan(journal, events, params, map, peer, role, admitted)
369
- else
370
- service_completion_plan(journal, events, params, map, peer, role, admitted)
371
- end
372
- end
373
- if request.fetch("operation") == "begin_dispatch" && result.fetch(:replayed)
374
- result = result.merge(data: result.fetch(:data).merge("invocation" => "already_started"))
375
- end
376
- if request.fetch("operation") == "request_service"
377
- # Acceptance metadata belongs to this exact mutation, while
378
- # retained service state may have advanced since acceptance.
379
- current = journal.service_request(params.fetch("request_id"))
380
- data = service_projection(current).merge(result.fetch(:data).slice("generation", "journal_commit"))
381
- data["claim"] = retained || result.fetch(:replayed) || result.dig(:data, "claim") == "retained" ? "retained" : "created"
382
- result = result.merge(data: data)
383
- end
384
- if %w[request_service begin_dispatch].include?(request.fetch("operation")) &&
385
- %w[prune-preserved-workspace publish].include?(result.fetch(:data).fetch("operation"))
386
- selector = request.fetch("operation") == "request_service" ? "request_event_digest" : "dispatch_event_digest"
387
- digest = cleanup_service_mutation_event!(journal, request, params, result.fetch(:data).fetch("journal_commit"))
388
- result = result.merge(data: result.fetch(:data).merge(selector => digest))
389
- end
390
- result
391
- end
392
- end
393
-
394
- # Selectors refer to the actual accepted mutation, never a caller's
395
- # pathname or the latest service record. They are derived after CAS,
396
- # avoiding a self-reference in the persisted mutation reply.
397
- def cleanup_service_mutation_event!(journal, request, params, commit)
398
- events = journal.read_events(params.fetch("assignment_id"), commit: commit)
399
- .select { |event| event["attempt_id"] == params.fetch("attempt_id") }
400
- unless Models::EvidenceEvent.chain_valid?(events)
401
- raise AttemptErrors::EvidenceUnavailable, "cleanup service mutation chain is unavailable"
402
- end
403
- selected = events.select do |event|
404
- event["type"] == "authority_mutation" && event.dig("payload", "mutation_id") == request.fetch("mutation_id")
405
- end
406
- event = selected.one? && selected.first
407
- unless event && event.dig("payload", "operation") == request.fetch("operation") &&
408
- event.dig("payload", "parameters_digest") == Atoms::EvidenceDigest.digest(params) &&
409
- event.dig("payload", "assignment_id") == params.fetch("assignment_id") &&
410
- event.dig("payload", "attempt_id") == params.fetch("attempt_id") &&
411
- event.dig("payload", "data", "request_id") == params.fetch("request_id") &&
412
- journal.event_commit!(assignment_id: params.fetch("assignment_id"), event_digest: event.fetch("digest"), commit: commit) == commit
413
- raise AttemptErrors::EvidenceUnavailable, "cleanup service mutation introduction is unavailable"
414
- end
415
- event.fetch("digest")
416
- end
417
-
418
- def service_policy!
419
- @service_policy || raise(ArgumentError, "full-service authority requires its installed policy owner")
420
- end
421
-
422
- def service_executor!(peer, role, record)
423
- unless record && role == :executor && peer["uid"] == record.fetch("executor_uid")
424
- raise AttemptErrors::UnauthorizedIdentity, "completion requires the recorded executor"
425
- end
426
- if record["operation"] == "publish" && record["dispatch_phase"] != "issued" && peer != record["executor_process_binding"]
427
- raise AttemptErrors::UnauthorizedIdentity, "publication requires its original receiver birth"
428
- end
429
- @kernel.live!(peer)
430
- true
431
- end
432
-
433
- def service_receiver!(peer, role, map, service_id)
434
- project = service_project!(map.fetch("project_id"))
435
- receiver = project.fetch("service_receivers").fetch(service_id)
436
- unless role == :executor && peer["uid"] == receiver.fetch("executor_uid") &&
437
- project.fetch("service_executor_uids").include?(peer["uid"]) &&
438
- !project.fetch("worker_uids").include?(peer["uid"])
439
- raise AttemptErrors::UnauthorizedIdentity, "service requires its fixed independent receiver"
440
- end
441
- @kernel.live!(peer)
442
- receiver
443
- rescue KeyError
444
- raise AttemptErrors::UnauthorizedIdentity, "fixed service receiver is unavailable"
445
- end
446
-
447
- def service_claim_plan(journal, events, params, map, peer, role, input_bytes)
448
- origin = service_origin!(events, params)
449
- worker = params.fetch("worker_process_binding")
450
- service_worker!(worker, :worker, map, origin)
451
- current = exact_candidate!(candidate(events), params)
452
- service_review!(journal, events, params, map, current, service_operation: params.fetch("operation")) unless %w[create update].include?(params.fetch("operation"))
453
- receiver = service_receiver!(peer, role, map, params.fetch("service_id"))
454
- binding = params.slice("request_id", "assignment_id", "attempt_id", "operation", "input_digest", "target",
455
- "authorization", "service_id").merge("project_id" => map.fetch("project_id"),
456
- "caller_uid" => map.fetch("worker_uid"), "mapping_id" => params.fetch("mapping_id"), "candidate_head" => current.fetch("head"),
457
- "candidate_generation" => current.fetch("candidate_generation"))
458
- .merge(service_origin_fields!(origin))
459
- prepared = service_policy!.prepare!(binding, input_bytes: input_bytes)
460
- binding = prepared.fetch(:binding).merge("policy_digest" => prepared.fetch(:policy_digest))
461
- unless binding["executor_uid"] == receiver.fetch("executor_uid")
462
- raise AttemptErrors::UnauthorizedIdentity, "policy and fixed receiver disagree"
463
- end
464
- existing = journal.service_request(binding.fetch("request_id"))
465
- if existing
466
- unless existing.slice(*SERVICE_IDENTITY) == binding.slice(*SERVICE_IDENTITY)
467
- raise AttemptErrors::Conflict, "service request immutable identity differs"
468
- end
469
- return {data: service_projection(existing).merge("claim" => "retained")}
470
- end
471
- ticket = SecureRandom.hex(16)
472
- record = binding.merge("dispatch_ticket_id" => ticket,
473
- "claim_binding" => Atoms::EvidenceDigest.digest(binding.merge("dispatch_ticket_id" => ticket)),
474
- "claim_generation" => 1, "dispatch_phase" => "issued", "state" => "uncertain",
475
- "claimed_at" => Time.now.utc.iso8601(9))
476
- {data: service_projection(record).merge("claim" => "created"), service_inputs: {record.fetch("request_id") => input_bytes}, service_updates: [{request_id: record.fetch("request_id"),
477
- expected: nil, replacement: record, event_type: "service_claim"}]}
478
- end
479
-
480
- def service_begin_plan(journal, events, params, map, peer, role, input_bytes)
481
- record = journal.service_request(params.fetch("request_id"))
482
- raise AttemptErrors::NotFound, "service request is unavailable" unless record
483
- service_receiver!(peer, role, map, record.fetch("service_id"))
484
- service_ticket!(record, params, map, peer)
485
- # A repeated begin never grants a second invocation, including after
486
- # lease expiry or terminality. It reports only retained ticket truth.
487
- return {data: service_projection(record).merge("invocation" => "already_started")} unless record["dispatch_phase"] == "issued"
488
- origin = service_origin!(events, params)
489
- unless service_origin_matches!(record, origin)
490
- raise AttemptErrors::UnauthorizedIdentity, "dispatch launch origin differs"
491
- end
492
- current = exact_candidate!(candidate(events), params)
493
- unless record["candidate_head"] == current["head"] && record["candidate_generation"] == current["candidate_generation"]
494
- raise AttemptErrors::Conflict, "dispatch candidate changed"
495
- end
496
- service_review!(journal, events, params, map, current, service_operation: record.fetch("operation")) unless %w[create update].include?(record.fetch("operation"))
497
- service_policy!.prepare!(record, input_bytes: input_bytes)
498
- replacement = record.merge("dispatch_phase" => "dispatch_started")
499
- if record.fetch("operation") == "prune-preserved-workspace"
500
- replacement["operation_owner_binding"] = service_policy!.dispatch_owner_binding!(record)
501
- end
502
- if %w[prune-preserved-workspace publish].include?(record.fetch("operation"))
503
- replacement["executor_process_binding"] = JSON.parse(JSON.generate(peer))
504
- end
505
- {data: service_projection(replacement).merge("invocation" => "permitted"),
506
- service_inputs: {record.fetch("request_id") => input_bytes},
507
- service_updates: [{request_id: record.fetch("request_id"), expected: record,
508
- replacement: replacement, event_type: "service_transition"}]}
509
- end
510
-
511
- def service_ticket!(record, params, map, peer)
512
- unless record["assignment_id"] == params["assignment_id"] && record["attempt_id"] == params["attempt_id"] &&
513
- record["project_id"] == map["project_id"] && record["executor_uid"] == peer["uid"] &&
514
- record["claim_binding"] == params["claim_binding"]
515
- raise AttemptErrors::UnauthorizedIdentity, "dispatch ticket binding differs"
516
- end
517
- true
518
- end
519
-
520
- def service_replay_binding!(record, params, map)
521
- fields = %w[request_id assignment_id attempt_id operation input_digest target authorization service_id mapping_id]
522
- unless fields.all? { |field| record[field] == params[field] } && record["project_id"] == map["project_id"] &&
523
- record["caller_uid"] == map["worker_uid"] && params.dig("worker_process_binding", "uid") == map["worker_uid"]
524
- raise AttemptErrors::Conflict, "service replay immutable identity differs"
525
- end
526
- @kernel.live!(params.fetch("worker_process_binding"))
527
- unless @kernel.descendant?(params.fetch("worker_process_binding"), record.fetch("worker_process_binding"))
528
- raise AttemptErrors::UnauthorizedIdentity, "service replay requires recorded native worker ownership"
529
- end
530
- true
531
- end
532
-
533
- def service_completion_plan(journal, events, params, map, peer, role, admitted)
534
- record = journal.service_request(params.fetch("request_id"))
535
- service_executor!(peer, role, record)
536
- service_ticket!(record, params, map, peer)
537
- digest = completion_binding!(record, params, admitted)
538
- if record["completion_digest"]
539
- return {data: service_projection(record)}
540
- end
541
- receipt = admitted.fetch(:receipt)
542
- if record["operation"] == "publish"
543
- unless admitted.fetch(:artifacts).size == 1
544
- raise AttemptErrors::ReceiptRejected, "publication requires its one exact result"
545
- end
546
- ServiceEvidence.new(journal: journal).publication_result!(admitted.fetch(:artifacts).first, record, state: receipt.fetch("outcome"))
547
- end
548
- owner = ServiceEvidence.new(journal: journal)
549
- canonical = Molecules::CanonicalEvidence.new(journal: journal)
550
- plan = canonical.import_plan(**owner.context(record), artifacts: admitted.fetch(:artifacts),
551
- admitted_after_event_digest: events.last.fetch("digest"))
552
- normalized = JSON.parse(JSON.generate(receipt)).merge("evidence" => plan.fetch(:references))
553
- replacement = record.merge("state" => receipt.fetch("outcome"), "receipt" => normalized, "completion_digest" => digest)
554
- replacement["failed_at"] = Time.now.utc.iso8601(9) if receipt["outcome"] == "failed"
555
- if %w[create update ready merge].include?(replacement["operation"]) && replacement["state"] == "succeeded"
556
- event = ServiceDeliveryEvidence.new(journal: journal).completion_event(record: replacement,
557
- receipt: normalized, completion_digest: digest, artifacts: admitted.fetch(:artifacts))
558
- plan = plan.merge(events: plan.fetch(:events) + [event])
559
- end
560
- plan.merge(data: service_projection(replacement), service_updates: [{request_id: record.fetch("request_id"),
561
- expected: record, replacement: replacement, event_type: "service_transition"}])
562
- end
563
-
564
- def completion_binding!(record, params, admitted)
565
- unless record["candidate_head"] == params["head"] && record["candidate_generation"] == params["candidate_generation"]
566
- raise AttemptErrors::Conflict, "completion candidate binding differs"
567
- end
568
- digest = Atoms::EvidenceDigest.digest("receipt_sha256" => admitted.fetch(:receipt_sha256),
569
- "artifacts" => admitted.fetch(:artifacts).map { |bytes| Digest::SHA256.hexdigest(bytes) })
570
- if record["completion_digest"] && record["completion_digest"] != digest
571
- raise AttemptErrors::Conflict, "service completion content differs"
572
- end
573
- receipt = admitted.fetch(:receipt)
574
- unless receipt.keys.sort == Molecules::EvidenceJournal::TERMINAL_RECEIPT_FIELDS.sort &&
575
- Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS.all? { |field| receipt[field] == record[field] } &&
576
- %w[succeeded failed].include?(receipt["outcome"]) &&
577
- (receipt["outcome"] != "succeeded" || record["dispatch_phase"] == "dispatch_started" || (record["operation"] == "publish" && record["dispatch_phase"] == "issuing"))
578
- raise AttemptErrors::ReceiptRejected, "completion must attest the exact dispatched request"
579
- end
580
- digest
581
- end
582
-
583
- def service_projection(record)
584
- record.slice("request_id", "assignment_id", "attempt_id", "project_id", "operation", "service_id", "target",
585
- "candidate_head", "candidate_generation", "state", "dispatch_ticket_id", "claim_binding", "claim_generation", "dispatch_phase", "policy_digest", "operation_owner_binding", "executor_process_binding", *ServicePublicationEvidence::PUBLICATION_FIELDS).tap do |projection|
586
- if record["operation"] == "publish" && %w[succeeded failed].include?(record["state"])
587
- projection.merge!(record.slice("receipt", "completion_digest", "authorization", "input_digest"))
588
- end
589
- end
590
- end
591
- end
592
- end
593
- end
594
- end