ace-assign 0.71.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 (195) 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 -2119
  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/organisms/assignment_executor.rb +0 -6
  35. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  36. data/lib/ace/assign/version.rb +1 -1
  37. data/lib/ace/assign.rb +0 -6
  38. metadata +4 -176
  39. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  40. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  41. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  42. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  43. data/docs/protected-authority.md +0 -282
  44. data/exe/ace-scope-ready +0 -55
  45. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  46. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  47. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  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/atoms/git_blob_batch.rb +0 -59
  52. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  53. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  54. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  55. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  56. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  57. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  58. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  59. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  60. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  61. data/lib/ace/assign/authority/client.rb +0 -257
  62. data/lib/ace/assign/authority/command_execution.rb +0 -180
  63. data/lib/ace/assign/authority/deployment.rb +0 -627
  64. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  65. data/lib/ace/assign/authority/endcap.rb +0 -457
  66. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  67. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  68. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  69. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  70. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  71. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  72. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  73. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  74. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  75. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  76. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  77. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  78. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  79. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  80. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  81. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  82. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  83. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  84. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  85. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  86. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  87. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  88. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  89. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  90. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  91. data/lib/ace/assign/authority/launch_review.rb +0 -91
  92. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  93. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  94. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  95. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  96. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  97. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  98. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  99. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  100. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  101. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  102. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  103. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  104. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  105. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  106. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  107. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  108. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  109. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  110. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  111. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  112. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  113. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  114. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  115. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  116. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  117. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  118. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  119. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  120. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  121. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  122. data/lib/ace/assign/authority/private_directory.rb +0 -33
  123. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  124. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  125. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  126. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  127. data/lib/ace/assign/authority/retained_input.rb +0 -53
  128. data/lib/ace/assign/authority/router.rb +0 -81
  129. data/lib/ace/assign/authority/server.rb +0 -131
  130. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  131. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  132. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  133. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  134. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  135. data/lib/ace/assign/authority/socket_server.rb +0 -255
  136. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  137. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  138. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  139. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  140. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  141. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  142. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  143. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  144. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  145. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  146. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  147. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  148. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  149. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  151. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  152. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  153. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  154. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  155. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  156. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  157. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  158. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  159. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  160. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  161. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  162. data/lib/ace/assign/models/attempt.rb +0 -178
  163. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  164. data/lib/ace/assign/models/evidence_event.rb +0 -86
  165. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  166. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  167. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  168. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  169. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  170. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  171. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  172. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  173. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  174. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  175. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  176. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  177. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  178. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  179. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  180. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  181. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  182. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  183. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  184. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  185. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  186. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  187. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  188. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  189. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  190. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  191. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  192. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  193. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  194. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  195. 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