ace-assign 0.63.1 → 0.64.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  3. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  5. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  7. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  8. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  9. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  10. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  11. data/CHANGELOG.md +43 -0
  12. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  13. data/docs/protected-authority.md +266 -0
  14. data/docs/usage.md +91 -2
  15. data/handbook/workflow-instructions/assign/drive.wf.md +2 -2
  16. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  17. data/lib/ace/assign/atoms/delivery_parameters.rb +54 -0
  18. data/lib/ace/assign/authority/candidate_transfer.rb +160 -0
  19. data/lib/ace/assign/authority/client.rb +107 -0
  20. data/lib/ace/assign/authority/deployment.rb +465 -0
  21. data/lib/ace/assign/authority/endcap.rb +342 -0
  22. data/lib/ace/assign/authority/endcap_inboxes.rb +314 -0
  23. data/lib/ace/assign/authority/endcap_results.rb +380 -0
  24. data/lib/ace/assign/authority/endcap_services.rb +395 -0
  25. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  26. data/lib/ace/assign/authority/execution_scope_observation.rb +476 -0
  27. data/lib/ace/assign/authority/launch_driver.rb +114 -0
  28. data/lib/ace/assign/authority/launch_lifecycle.rb +822 -0
  29. data/lib/ace/assign/authority/launch_scope_abort.rb +88 -0
  30. data/lib/ace/assign/authority/launch_scope_admission.rb +84 -0
  31. data/lib/ace/assign/authority/launch_scope_close.rb +115 -0
  32. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  33. data/lib/ace/assign/authority/launch_scope_release.rb +180 -0
  34. data/lib/ace/assign/authority/posix_acl.rb +56 -0
  35. data/lib/ace/assign/authority/private_directory.rb +33 -0
  36. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  37. data/lib/ace/assign/authority/router.rb +54 -0
  38. data/lib/ace/assign/authority/server.rb +230 -0
  39. data/lib/ace/assign/authority/service_evidence.rb +55 -0
  40. data/lib/ace/assign/authority/transfer_codec.rb +155 -0
  41. data/lib/ace/assign/cli/commands/authority/launch.rb +45 -0
  42. data/lib/ace/assign/cli/commands/authority/serve.rb +30 -0
  43. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  44. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  45. data/lib/ace/assign/cli/commands/create.rb +9 -1
  46. data/lib/ace/assign/cli/commands/delivery.rb +57 -0
  47. data/lib/ace/assign/cli/commands/inbox_reconcile.rb +36 -0
  48. data/lib/ace/assign/cli/commands/resume.rb +26 -0
  49. data/lib/ace/assign/cli.rb +22 -1
  50. data/lib/ace/assign/models/evidence_event.rb +3 -1
  51. data/lib/ace/assign/molecules/attempt_reconciler.rb +31 -12
  52. data/lib/ace/assign/molecules/canonical_evidence.rb +150 -0
  53. data/lib/ace/assign/molecules/evidence_journal.rb +218 -63
  54. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  55. data/lib/ace/assign/molecules/execution_scope_lineage.rb +327 -0
  56. data/lib/ace/assign/molecules/journal_mutation.rb +202 -0
  57. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +4 -0
  58. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  59. data/lib/ace/assign/molecules/receipt_verifier.rb +49 -19
  60. data/lib/ace/assign/organisms/assignment_executor.rb +6 -0
  61. data/lib/ace/assign/organisms/attempt_coordinator.rb +312 -13
  62. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  63. data/lib/ace/assign/organisms/task_assignment_creator.rb +8 -3
  64. data/lib/ace/assign/version.rb +1 -1
  65. data/lib/ace/assign.rb +13 -0
  66. metadata +107 -8
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/herdr/organisms/protected_inbox"
4
+ require_relative "../molecules/execution_scope_lineage"
5
+
6
+ module Ace
7
+ module Assign
8
+ module Authority
9
+ class Endcap
10
+ INBOX_REGISTRATION_FIELDS = %w[event_id attempt_id payload_sha256 receipt_key_sha256].freeze
11
+ INBOX_PAYLOAD_FIELDS = %w[version event_id attempt_id inbox_context_id registration state
12
+ claim_generation binding receipt_ref signature_ref].freeze
13
+ INBOX_BINDING_FIELDS = %w[project_id assignment_id attempt_id mapping_id inbox_context_id event_id
14
+ registration claim_generation native_binding scope_binding_event_id native_binding_event_id
15
+ scope_native_binding receipt_key_sha256 receipt_sha256 signature_sha256 submitter_uid submitter_role].freeze
16
+ INBOX_REF_FIELDS = %w[artifact_id ref sha256 bytes].freeze
17
+
18
+ # Shared source-owned predicate for existing scope abort/reuse/finish.
19
+ # The caller holds lifecycle exclusion; this is never a wire operation.
20
+ def inbox_settlement_complete!(journal:, events:, params:, map:, commit:)
21
+ protected_journal!(journal)
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, "inbox settlement requires exact canonical commit"
26
+ end
27
+ registrations = events.select { |event| event["type"] == "inbox_binding" }
28
+ project = @deployment.project(map.fetch("project_id"))
29
+ project.fetch("inbox_contexts", {}).each_key do |id|
30
+ context = @deployment.inbox_context(params.fetch("mapping_id"), id)
31
+ next unless context.fetch("native_mapping_id") == params.fetch("mapping_id")
32
+ @deployment.verify_inbox_context!(params.fetch("mapping_id"), id)
33
+ actual = Ace::Herdr::Organisms::Inbox.retained_events(deliveries_dir: context.fetch("deliveries_dir"), attempt: params.fetch("attempt_id"))
34
+ registered = registrations.filter_map { |event| event.dig("payload", "event_id") if event.dig("payload", "inbox_context_id") == id }
35
+ unless actual.sort == registered.sort
36
+ raise AttemptErrors::EvidenceUnavailable, "registered or unattributable inbox retention differs"
37
+ end
38
+ end
39
+ registrations.each do |event|
40
+ selected = params.merge("event_id" => event.dig("payload", "event_id"), "inbox_context_id" => event.dig("payload", "inbox_context_id"))
41
+ box, registration, lineage = inbox_environment(events, selected, map)
42
+ proof = verified_inbox_record(journal, events, selected, map, commit, box: box, registration: registration, lineage: lineage)
43
+ unless proof && proof["state"] == "completed"
44
+ raise AttemptErrors::EvidenceUnavailable, "current inbox settlement is incomplete"
45
+ end
46
+ end
47
+ true
48
+ rescue KeyError, TypeError, ArgumentError, Ace::Herdr::Error, Ace::Runtime::RuntimeUnavailableError
49
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox settlement is unverifiable"
50
+ end
51
+
52
+ private
53
+
54
+ def inbox_request(request)
55
+ params = request.fetch("params")
56
+ unless params.is_a?(Hash) && params.keys.sort == PARAMETERS.fetch("reconcile_inbox").sort
57
+ raise ArgumentError, "invalid inbox parameters"
58
+ end
59
+ %w[mapping_id assignment_id attempt_id event_id inbox_context_id].each { |key| result_id!(params[key]) }
60
+ result_id!(request.fetch("mutation_id"))
61
+ registration = params.fetch("expected_registration")
62
+ unless inbox_registration?(registration) && registration.values_at("event_id", "attempt_id") ==
63
+ params.values_at("event_id", "attempt_id") &&
64
+ params["expected_generation"].is_a?(Integer) && params["expected_generation"] >= 0 &&
65
+ %w[receipt_sha256 signature_sha256].all? { |key| params[key].is_a?(String) && DIGEST.match?(params[key]) }
66
+ raise ArgumentError, "invalid inbox registration or digest"
67
+ end
68
+ map = @deployment.mapping(params.fetch("mapping_id"))
69
+ raise AttemptErrors::UnauthorizedIdentity, "project mapping differs" unless map.fetch("project_id") == request.fetch("project_id")
70
+ [params, map]
71
+ end
72
+
73
+ def inbox_registration?(value)
74
+ value.is_a?(Hash) && value.keys.sort == INBOX_REGISTRATION_FIELDS.sort &&
75
+ %w[event_id attempt_id].all? { |key| value[key].is_a?(String) && Molecules::JournalMutation::ID.match?(value[key]) } &&
76
+ %w[payload_sha256 receipt_key_sha256].all? { |key| value[key].is_a?(String) && DIGEST.match?(value[key]) }
77
+ end
78
+
79
+ def inbox_selection(events, params, map, peer, role)
80
+ origin = retained_origin(events, params)
81
+ context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
82
+ @kernel.live!(peer)
83
+ service_policy!.visible!(project: map.fetch("project_id"), uid: peer.fetch("uid"))
84
+ allowed = (role == :supervisor && context.fetch("supervisor_uids").include?(peer["uid"])) ||
85
+ (role == :launcher && peer["uid"] == map.fetch("launcher_uid") && @kernel.same?(peer, origin.fetch("launcher_identity")))
86
+ raise AttemptErrors::UnauthorizedIdentity, "inbox requires exact launcher or mapped supervisor" unless allowed
87
+ inbox_environment(events, params, map)
88
+ end
89
+
90
+ def inbox_environment(events, params, map)
91
+ unless Models::EvidenceEvent.chain_valid?(events) && events.all? { |event| event["attempt_id"] == params.fetch("attempt_id") }
92
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox attempt chain differs"
93
+ end
94
+ context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
95
+ @deployment.verify_inbox_context!(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
96
+ bindings = events.select { |event| event["type"] == "inbox_binding" && event.dig("payload", "event_id") == params["event_id"] }
97
+ raise AttemptErrors::NotFound, "inbox registration missing" if bindings.empty?
98
+ registered = bindings.one? && bindings.first.fetch("payload")
99
+ unless registered.is_a?(Hash) && registered.keys.sort == %w[event_id attempt_id inbox_context_id registration].sort &&
100
+ registered.values_at("event_id", "attempt_id", "inbox_context_id") == params.values_at("event_id", "attempt_id", "inbox_context_id") &&
101
+ inbox_registration?(registered["registration"])
102
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox context or registration differs"
103
+ end
104
+ if params.key?("expected_registration") && registered.fetch("registration") != params.fetch("expected_registration")
105
+ raise AttemptErrors::Conflict, "expected inbox registration differs"
106
+ end
107
+ lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
108
+ assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
109
+ mapping_id: context.fetch("native_mapping_id"))
110
+ raise AttemptErrors::EvidenceUnavailable, "canonical original native stage missing" unless lineage.native_event
111
+ native_map = @deployment.mapping(context.fetch("native_mapping_id"))
112
+ box = Ace::Herdr::Organisms::ProtectedInbox.build(context: context, mapping: native_map,
113
+ native: lineage.native_event.fetch("payload"), kernel: @kernel)
114
+ current = box.retained_status(event: params.fetch("event_id"))
115
+ unless current.slice(*INBOX_REGISTRATION_FIELDS) == registered.fetch("registration")
116
+ raise AttemptErrors::EvidenceUnavailable, "current inbox differs from canonical registration"
117
+ end
118
+ inbox_native_lineage!(current, lineage)
119
+ [box, registered.fetch("registration"), lineage]
120
+ rescue Ace::Herdr::Error, Ace::Runtime::RuntimeUnavailableError, KeyError, TypeError
121
+ raise AttemptErrors::EvidenceUnavailable, "installed inbox or native lineage is unverifiable"
122
+ end
123
+
124
+ def inbox_native_lineage!(record, lineage, receipt: nil)
125
+ workspace = lineage.native_event.fetch("payload").fetch("workspace_id")
126
+ original = record.fetch("origin_target")
127
+ binding = record.fetch("binding")
128
+ unless original.is_a?(Hash) && binding.is_a?(Hash) &&
129
+ original["session"] == workspace && binding["session"] == workspace
130
+ raise AttemptErrors::EvidenceUnavailable, "inbox original native workspace differs"
131
+ end
132
+ if lineage.child_event
133
+ child = lineage.child_event.fetch("payload").fetch("original_process_binding")
134
+ unless original.values_at("session", "pane", "terminal_id") == child.values_at("session", "pane", "terminal_id")
135
+ raise AttemptErrors::EvidenceUnavailable, "inbox original native child differs"
136
+ end
137
+ end
138
+ if receipt
139
+ signed_binding = receipt["binding"]
140
+ replacement = receipt["replacement_target"]
141
+ unless signed_binding.is_a?(Hash) && signed_binding["session"] == workspace &&
142
+ (!receipt.key?("replacement_target") || (replacement.is_a?(Hash) && replacement["session"] == workspace))
143
+ raise AttemptErrors::EvidenceUnavailable, "signed inbox native workspace differs"
144
+ end
145
+ end
146
+ true
147
+ end
148
+
149
+ def authorize_inbox_transfer!(request:, peer:, role:)
150
+ params, map = inbox_request(request)
151
+ @launch.with_assignment(params: params, map: map) do |journal, _|
152
+ protected_journal!(journal)
153
+ inbox_selection(attempt_events(journal, params), params, map, peer, role)
154
+ end
155
+ true
156
+ end
157
+
158
+ def dispatch_inbox(request:, peer:, role:, transfer:)
159
+ params, map = inbox_request(request)
160
+ authorize_inbox_transfer!(request: request, peer: peer, role: role)
161
+ unless transfer && transfer.count == 2
162
+ raise ArgumentError, "inbox requires receipt and signature"
163
+ end
164
+ bytes, signature = [transfer.bytes(index: 0), transfer.bytes(index: 1)]
165
+ unless [bytes, signature].all? { |part| part.is_a?(String) && part.bytesize.between?(1, 16 * 1024) } &&
166
+ Digest::SHA256.hexdigest(bytes) == params.fetch("receipt_sha256") &&
167
+ Digest::SHA256.hexdigest(signature) == params.fetch("signature_sha256")
168
+ raise ArgumentError, "inbox proof bytes differ"
169
+ end
170
+ receipt = JSON.parse(bytes.dup.force_encoding(Encoding::UTF_8))
171
+ @launch.with_assignment(params: params, map: map) do |journal, _|
172
+ protected_journal!(journal)
173
+ commit = journal.ref_value
174
+ events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
175
+ box, registration, lineage = inbox_selection(events, params, map, peer, role)
176
+ replay = journal.mutation_result(request.fetch("mutation_id"))
177
+ if replay && replay["operation"] == "reconcile_inbox"
178
+ retained = verified_inbox(journal, events, params, map, peer, role, commit)
179
+ unless retained && inbox_projection(retained) == replay.fetch("data").slice(*inbox_projection(retained).keys)
180
+ raise AttemptErrors::EvidenceUnavailable, "retained inbox reply differs"
181
+ end
182
+ end
183
+ result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
184
+ mutation_id: request.fetch("mutation_id"), operation: "reconcile_inbox", parameters_digest: Atoms::EvidenceDigest.digest(params),
185
+ expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, fresh_commit, _generation|
186
+ box, registration, lineage = inbox_selection(fresh, params, map, peer, role)
187
+ retained = verified_inbox(journal, fresh, params, map, peer, role, fresh_commit)
188
+ if retained
189
+ unless retained.dig("binding", "receipt_sha256") == params.fetch("receipt_sha256") &&
190
+ retained.dig("binding", "signature_sha256") == params.fetch("signature_sha256")
191
+ raise AttemptErrors::Conflict, "canonical inbox proof already differs"
192
+ end
193
+ next({data: inbox_projection(retained)})
194
+ end
195
+ inbox_native_lineage!(box.retained_status(event: params.fetch("event_id")), lineage, receipt: receipt)
196
+ accepted = box.reconcile(event: params.fetch("event_id"), receipt: receipt, signed_bytes: bytes,
197
+ signature: signature, expected_registration: registration)
198
+ raise AttemptErrors::EvidenceUnavailable, "signed inbox proof refused" if accepted["reconciliation_refusal"]
199
+ binding = inbox_binding(params, map, peer, role, registration, lineage, accepted)
200
+ plan = Molecules::CanonicalEvidence.new(journal: journal).import_plan(**inbox_context(binding),
201
+ admitted_after_event_digest: fresh.last.fetch("digest"), artifacts: [bytes, signature])
202
+ refs = plan.fetch(:references).each_with_index.map do |ref, index|
203
+ ref.merge("artifact_id" => ref.fetch("ref").delete_prefix("evidence/imports/"), "bytes" => [bytes, signature][index].bytesize)
204
+ end
205
+ payload = {"version" => 1, "event_id" => params.fetch("event_id"), "attempt_id" => params.fetch("attempt_id"),
206
+ "inbox_context_id" => params.fetch("inbox_context_id"), "registration" => registration,
207
+ "state" => accepted.fetch("state"), "claim_generation" => accepted.fetch("claim_generation"),
208
+ "binding" => binding, "receipt_ref" => refs[0], "signature_ref" => refs[1]}
209
+ {data: inbox_projection(payload), blobs: plan.fetch(:blobs), events: plan.fetch(:events) +
210
+ [{type: "inbox_reconciliation", payload: payload}]}
211
+ end
212
+ final_commit = journal.ref_value
213
+ final_events = journal.read_events(params.fetch("assignment_id"), commit: final_commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
214
+ retained = verified_inbox(journal, final_events, params, map, peer, role, final_commit)
215
+ unless retained && inbox_projection(retained) == result.fetch(:data).slice(*inbox_projection(retained).keys)
216
+ raise AttemptErrors::EvidenceUnavailable, "accepted inbox provenance differs"
217
+ end
218
+ result
219
+ end
220
+ rescue JSON::ParserError, EncodingError, Ace::Herdr::Error
221
+ raise AttemptErrors::EvidenceUnavailable, "inbox proof is unverifiable"
222
+ end
223
+
224
+ def inbox_binding(params, map, peer, role, registration, lineage, accepted)
225
+ {"project_id" => map.fetch("project_id"), "assignment_id" => params.fetch("assignment_id"),
226
+ "attempt_id" => params.fetch("attempt_id"), "mapping_id" => params.fetch("mapping_id"),
227
+ "inbox_context_id" => params.fetch("inbox_context_id"), "event_id" => params.fetch("event_id"),
228
+ "registration" => registration, "claim_generation" => accepted.fetch("claim_generation"),
229
+ "native_binding" => accepted.fetch("binding"), "scope_binding_event_id" => lineage.binding_event.fetch("digest"),
230
+ "native_binding_event_id" => lineage.native_event.fetch("digest"), "scope_native_binding" => lineage.native_event.fetch("payload"),
231
+ "receipt_key_sha256" => registration.fetch("receipt_key_sha256"), "receipt_sha256" => params.fetch("receipt_sha256"),
232
+ "signature_sha256" => params.fetch("signature_sha256"), "submitter_uid" => peer.fetch("uid"), "submitter_role" => role.to_s}
233
+ end
234
+
235
+ def inbox_context(binding)
236
+ {kind: "inbox", project_id: binding.fetch("project_id"), assignment_id: binding.fetch("assignment_id"),
237
+ attempt_id: binding.fetch("attempt_id"), peer_uid: binding.fetch("submitter_uid"), binding: binding,
238
+ request_id_or_event_id: binding.fetch("event_id"), generation: binding.fetch("claim_generation")}
239
+ end
240
+
241
+ def inbox_projection(payload)
242
+ payload.slice("event_id", "attempt_id", "inbox_context_id", "registration", "state", "receipt_ref", "signature_ref")
243
+ end
244
+
245
+ def verified_inbox(journal, events, params, map, peer, role, commit)
246
+ box, registration, lineage = inbox_selection(events, params, map, peer, role)
247
+ verified_inbox_record(journal, events, params, map, commit, box: box, registration: registration, lineage: lineage)
248
+ end
249
+
250
+ def verified_inbox_record(journal, events, params, map, commit, box:, registration:, lineage:)
251
+ records = events.select { |event| event["type"] == "inbox_reconciliation" && event.dig("payload", "event_id") == params.fetch("event_id") }
252
+ replies = events.select { |event| event["type"] == "authority_mutation" &&
253
+ event.dig("payload", "operation") == "reconcile_inbox" && event.dig("payload", "data", "event_id") == params.fetch("event_id") }
254
+ unless replies.all? { |reply| records.any? { |record| inbox_projection(record.fetch("payload")) ==
255
+ reply.fetch("payload").fetch("data").slice(*inbox_projection(record.fetch("payload")).keys) } }
256
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox private provenance missing"
257
+ end
258
+ observed = box.retained_status(event: params.fetch("event_id"))
259
+ current_claim = observed.fetch("claim_generation")
260
+ unless records.all? { |record| record.fetch("payload").keys.sort == INBOX_PAYLOAD_FIELDS.sort &&
261
+ record.dig("payload", "claim_generation").is_a?(Integer) && record.dig("payload", "claim_generation").between?(0, current_claim) } &&
262
+ records.map { |record| record.dig("payload", "claim_generation") }.uniq.length == records.length
263
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox claim history differs"
264
+ end
265
+ current = records.find { |record| record.dig("payload", "claim_generation") == current_claim }
266
+ return nil unless current
267
+ payload = current.fetch("payload")
268
+ binding = payload.fetch("binding")
269
+ unless payload.keys.sort == INBOX_PAYLOAD_FIELDS.sort && payload["version"] == 1 && binding.keys.sort == INBOX_BINDING_FIELDS.sort &&
270
+ %w[receipt_sha256 signature_sha256].all? { |key| binding[key].is_a?(String) && DIGEST.match?(binding[key]) } &&
271
+ binding["submitter_uid"].is_a?(Integer) && binding["submitter_uid"] > 0 && %w[launcher supervisor].include?(binding["submitter_role"]) &&
272
+ payload.values_at("event_id", "attempt_id", "inbox_context_id") == params.values_at("event_id", "attempt_id", "inbox_context_id") &&
273
+ payload["registration"] == registration && payload["claim_generation"] == binding["claim_generation"]
274
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox fields differ"
275
+ end
276
+ retained_peer = {"uid" => binding.fetch("submitter_uid")}
277
+ expected = inbox_binding(params.merge("receipt_sha256" => binding.fetch("receipt_sha256"),
278
+ "signature_sha256" => binding.fetch("signature_sha256")), map, retained_peer, binding.fetch("submitter_role").to_sym,
279
+ registration, lineage, observed)
280
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox native/key context differs" unless binding == expected
281
+ canonical = Molecules::CanonicalEvidence.new(journal: journal)
282
+ raw = %w[receipt_ref signature_ref].map do |key|
283
+ ref = payload.fetch(key)
284
+ unless ref.is_a?(Hash) && ref.keys.sort == INBOX_REF_FIELDS.sort &&
285
+ ref["ref"] == "evidence/imports/#{ref['artifact_id']}" && ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 16 * 1024)
286
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox reference differs"
287
+ end
288
+ bytes = canonical.read(ref.slice("ref", "sha256"), **inbox_context(binding), commit: commit)
289
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox byte count differs" unless bytes.bytesize == ref["bytes"]
290
+ bytes
291
+ end
292
+ unless raw.map { |bytes| Digest::SHA256.hexdigest(bytes) } == binding.values_at("receipt_sha256", "signature_sha256")
293
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox raw digests differ"
294
+ end
295
+ receipt = JSON.parse(raw.first)
296
+ inbox_native_lineage!(observed, lineage, receipt: receipt)
297
+ verified = box.verify_reconciliation(event: params.fetch("event_id"), receipt: receipt,
298
+ signed_bytes: raw.first, signature: raw.last, expected_registration: registration)
299
+ signed_state = receipt["outcome"] == "consumed" ? "completed" : "queued"
300
+ if verified["reconciliation_refusal"] || verified["state"] != payload["state"] || payload["state"] != signed_state
301
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox signature or settlement differs"
302
+ end
303
+ unless events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reconcile_inbox" &&
304
+ event.fetch("payload").fetch("data").slice(*inbox_projection(payload).keys) == inbox_projection(payload) }
305
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox owner reply missing"
306
+ end
307
+ payload
308
+ rescue KeyError, TypeError, ArgumentError, JSON::ParserError, Ace::Herdr::Error
309
+ raise AttemptErrors::EvidenceUnavailable, "canonical inbox evidence is unverifiable"
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end